// Copyright 2018-2024 CERN
//
// 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.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package ocdav

import (
	"context"
	"crypto/tls"
	"fmt"
	"net/http"
	"net/url"
	"path"
	"regexp"
	"strings"
	"time"

	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
	storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
	"github.com/cs3org/reva/v3/pkg/appctx"
	"github.com/cs3org/reva/v3/pkg/myofficefiles"
	"github.com/cs3org/reva/v3/pkg/spaces"
	"github.com/cs3org/reva/v3/pkg/trace"
	"github.com/cs3org/reva/v3/pkg/utils"

	"github.com/cs3org/reva/v3/pkg/errtypes"
	"github.com/cs3org/reva/v3/pkg/httpclient"
	"github.com/cs3org/reva/v3/pkg/notification/notificationhelper"
	"github.com/cs3org/reva/v3/pkg/rgrpc/todo/pool"
	"github.com/cs3org/reva/v3/pkg/rhttp/global"
	"github.com/cs3org/reva/v3/pkg/rhttp/router"
	"github.com/cs3org/reva/v3/pkg/sharedconf"
	"github.com/cs3org/reva/v3/pkg/storage/favorite"
	"github.com/cs3org/reva/v3/pkg/storage/favorite/registry"
	"github.com/cs3org/reva/v3/pkg/utils/cfg"
	"github.com/pkg/errors"
)

type ctxKey int

const (
	ctxKeyBaseURI ctxKey = iota
	ctxSpaceID
	ctxSpacePath
	ctxOCM
	ctxPublicLink
	ctxStorageId
	ctxResourceOpaqueId
)

var (
	errInvalidValue = errors.New("invalid value")

	nameRules = [...]nameRule{
		nameNotEmpty{},
		nameDoesNotContain{chars: "\f\r\n\\"},
	}
)

type nameRule interface {
	Test(name string) bool
}

type nameNotEmpty struct{}

func (r nameNotEmpty) Test(name string) bool {
	return len(strings.TrimSpace(name)) > 0
}

type nameDoesNotContain struct {
	chars string
}

func (r nameDoesNotContain) Test(name string) bool {
	return !strings.ContainsAny(name, r.chars)
}

func init() {
	global.Register("ocdav", New)
}

type ConfigPublicLinkDownload struct {
	MaxNumFiles  int64  `mapstructure:"max_num_files"`
	MaxSize      int64  `mapstructure:"max_size"`
	PublicFolder string `mapstructure:"public_folder"`
}

// Config holds the config options that need to be passed down to all ocdav handlers.
type Config struct {
	Prefix string `mapstructure:"prefix"`
	// FilesNamespace prefixes the namespace, optionally with user information.
	// Example: if FilesNamespace is /users/{{substr 0 1 .Username}}/{{.Username}}
	// and received path is /docs the internal path will be:
	// /users/<first char of username>/<username>/docs
	FilesNamespace string `mapstructure:"files_namespace"`
	// WebdavNamespace prefixes the namespace, optionally with user information.
	// Example: if WebdavNamespace is /users/{{substr 0 1 .Username}}/{{.Username}}
	// and received path is /docs the internal path will be:
	// /users/<first char of username>/<username>/docs
	WebdavNamespace string `mapstructure:"webdav_namespace"`
	OCMNamespace    string `mapstructure:"ocm_namespace"`
	GatewaySvc      string `mapstructure:"gatewaysvc"`
	Timeout         int64  `mapstructure:"timeout"`
	Insecure        bool   `docs:"false;Whether to skip certificate checks when sending requests." mapstructure:"insecure"`
	// If true, HTTP COPY will expect the HTTP-TPC (third-party copy) headers
	EnableHTTPTpc bool `mapstructure:"enable_http_tpc"`
	// The authentication scheme to use for the tpc push call when userinfo part is specified in the Destination header uri. Default value is 'bearer'.
	// Possible values:
	// "bearer"				results in header: Authorization: Bearer ...token...
	// "x-access-token":	results in header: X-Access-Token: ...token...
	HTTPTpcPushAuthHeader        string                    `mapstructure:"http_tpc_push_auth_header"`
	PublicURL                    string                    `mapstructure:"public_url"`
	FavoriteStorageDriver        string                    `mapstructure:"favorite_storage_driver"`
	FavoriteStorageDrivers       map[string]map[string]any `mapstructure:"favorite_storage_drivers"`
	PublicLinkDownload           *ConfigPublicLinkDownload `mapstructure:"publiclink_download"`
	DisabledOpenInAppPaths       []string                  `mapstructure:"disabled_open_in_app_paths"`
	Notifications                map[string]any            `docs:"nil; settings for the notification helper" mapstructure:"notifications"`
	MyOfficeFilesAllowedProjects []string                  `mapstructure:"my_office_files_projects"`
}

func (c *Config) ApplyDefaults() {
	// note: default c.Prefix is an empty string
	c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)

	if c.FavoriteStorageDriver == "" {
		c.FavoriteStorageDriver = "memory"
	}

	if c.OCMNamespace == "" {
		c.OCMNamespace = "/ocm"
	}

	if len(c.MyOfficeFilesAllowedProjects) == 0 {
		c.MyOfficeFilesAllowedProjects = []string{"cernbox"}
	}
}

type svc struct {
	c                    *Config
	webDavHandler        *WebDavHandler
	davHandler           *DavHandler
	favoritesManager     favorite.Manager
	myOfficeFilesManager myofficefiles.Manager
	client               *httpclient.Client
	// Can be nil if notifications are not set up
	notificationHelper   *notificationhelper.NotificationHelper
}

func getFavoritesManager(c *Config) (favorite.Manager, error) {
	if f, ok := registry.NewFuncs[c.FavoriteStorageDriver]; ok {
		return f(c.FavoriteStorageDrivers[c.FavoriteStorageDriver])
	}
	return nil, errtypes.NotFound("driver not found: " + c.FavoriteStorageDriver)
}

// New returns a new ocdav.
func New(ctx context.Context, m map[string]any) (global.Service, error) {
	var c Config
	if err := cfg.Decode(m, &c); err != nil {
		return nil, err
	}

	fm, err := getFavoritesManager(&c)
	if err != nil {
		return nil, err
	}

	myOfficeFilesManager, err := myofficefiles.New(ctx, c.GatewaySvc, c.MyOfficeFilesAllowedProjects)
	if err != nil {
		return nil, err
	}

	log := appctx.GetLogger(ctx)
	tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: c.Insecure}}


	s := &svc{
		c:             &c,
		webDavHandler: new(WebDavHandler),
		davHandler:    new(DavHandler),
		client: httpclient.New(
			httpclient.Timeout(time.Duration(c.Timeout*int64(time.Second))),
			httpclient.RoundTripper(tr),
		),
		favoritesManager:     fm,
		myOfficeFilesManager: myOfficeFilesManager,
	}
	if c.Notifications != nil {
		nh, err := notificationhelper.New("ocdav", c.Notifications, log)
		if err != nil {
			return nil, err
		}
		s.notificationHelper = nh
	}
	
	// initialize handlers and set default cigs
	if err := s.webDavHandler.init(c.WebdavNamespace, true); err != nil {
		return nil, err
	}
	if err := s.davHandler.init(&c); err != nil {
		return nil, err
	}
	return s, nil
}

func (s *svc) Prefix() string {
	return s.c.Prefix
}

func (s *svc) Close() error {
	if s.notificationHelper != nil {
		s.notificationHelper.Stop()
	}
	return nil
}

func (s *svc) Unprotected() []string {
	return []string{"/status.php", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/s/", "/remote.php/dav/ocm/", "/ocm-provider"}
}

func (s *svc) Handler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		log := appctx.GetLogger(ctx)
		w.Header().Set("x-request-id", trace.Get(ctx))

		addAccessHeaders(w, r)

		// TODO(jfd): do we need this?
		// fake litmus testing for empty namespace: see https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/litmus_test_server.go#L58-L89
		if r.Header.Get("X-Litmus") == "props: 3 (propfind_invalid2)" {
			http.Error(w, "400 Bad Request", http.StatusBadRequest)
			return
		}

		// to build correct href prop urls we need to keep track of the base path
		// always starts with /
		base := path.Join("/", s.Prefix())

		// We store the actual incoming URL
		ctx = context.WithValue(ctx, ctxKeyIncomingURL, r.URL.Path)
		r = r.WithContext(ctx)

		var head string
		head, r.URL.Path = router.ShiftPath(r.URL.Path)
		log.Debug().Str("head", head).Str("tail", r.URL.Path).Msg("http routing")
		switch head {
		case "s":
			if strings.HasSuffix(r.URL.Path, "/download") {
				r.URL.Path = strings.TrimSuffix(r.URL.Path, "/download")
				s.handleLegacyPublicLinkDownload(w, r)
				return
			}
			http.Error(w, "Not Yet Implemented", http.StatusNotImplemented)
			return
		case "status.php":
			s.doStatus(w, r)
			return
		case "remote.php":
			// skip optional "remote.php"
			head, r.URL.Path = router.ShiftPath(r.URL.Path)

			// yet, add it to baseURI
			base = path.Join(base, "remote.php")
		case "apps":
			head, r.URL.Path = router.ShiftPath(r.URL.Path)
			if head == "files" {
				s.handleLegacyPath(w, r)
				return
			}
		case "index.php":
			head, r.URL.Path = router.ShiftPath(r.URL.Path)
			if head == "s" {
				token := r.URL.Path
				rURL := s.c.PublicURL + path.Join(head, token)
				r.URL.Path = "/" // reset old path for redirection
				http.Redirect(w, r, rURL, http.StatusMovedPermanently)
				return
			}
		case "ocm-provider":
			// this is to support the current/legacy discovery endpoint for OCM
			http.Redirect(w, r, "/.well-known/ocm", http.StatusMovedPermanently)
			return
		}
		switch head {
		// the old `/webdav` endpoint uses remote.php/webdav/$path
		case "webdav":
			// for oc we need to prepend /home as the path that will be passed to the home storage provider
			// will not contain the username
			base = path.Join(base, "webdav")
			ctx := context.WithValue(ctx, ctxKeyBaseURI, base)
			r = r.WithContext(ctx)
			s.webDavHandler.Handler(s).ServeHTTP(w, r)
			return
		case "dav":
			// cern uses /dav/files/$namespace -> /$namespace/...
			// oc uses /dav/files/$user -> /$home/$user/...
			// for oc we need to prepend the path to user homes
			// or we take the path starting at /dav and allow rewriting it?
			base = path.Join(base, "dav")
			ctx := context.WithValue(ctx, ctxKeyBaseURI, base)
			r = r.WithContext(ctx)
			s.davHandler.Handler(s).ServeHTTP(w, r)
			return
		}
		log.Warn().Msg("resource not found")
		w.WriteHeader(http.StatusNotFound)
	})
}

func (s *svc) getClient() (gateway.GatewayAPIClient, error) {
	return pool.GetGatewayServiceClient(pool.Endpoint(s.c.GatewaySvc))
}

func applyLayout(ctx context.Context, ns string, useLoggedInUserNS bool, requestPath string) string {
	return ns
	// If useLoggedInUserNS is false, that implies that the request is coming from
	// the FilesHandler method invoked by a /dav/files/fileOwner where fileOwner
	// is not the same as the logged in user. In that case, we'll treat fileOwner
	// as the username whose files are to be accessed and use that in the
	// namespace template.
	/*
		u, ok := appctx.ContextGetUser(ctx)
		if !ok || !useLoggedInUserNS {
			requestUserID, _ := router.ShiftPath(requestPath)
			u = &userpb.User{
				Username: requestUserID,
			}
		}
		return templates.WithUser(u, ns)
	*/
}

func addAccessHeaders(w http.ResponseWriter, r *http.Request) {
	headers := w.Header()
	// the webdav api is accessible from anywhere
	headers.Set(HeaderAccessControlAllowOrigin, "*")
	// all resources served via the DAV endpoint should have the strictest possible as default
	headers.Set("Content-Security-Policy", "default-src 'none';")
	// disable sniffing the content type for IE
	headers.Set("X-Content-Type-Options", "nosniff")
	// https://msdn.microsoft.com/en-us/library/jj542450(v=vs.85).aspx
	headers.Set("X-Download-Options", "noopen")
	// Disallow iFraming from other domains
	headers.Set("X-Frame-Options", "SAMEORIGIN")
	// https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html
	headers.Set("X-Permitted-Cross-Domain-Policies", "none")
	// https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag
	headers.Set("X-Robots-Tag", "none")
	// enforce browser based XSS filters
	headers.Set("X-XSS-Protection", "1; mode=block")

	if r.TLS != nil {
		headers.Set("Strict-Transport-Security", "max-age=63072000")
	}
}

func extractDestination(r *http.Request, ns string) (string, error) {
	dstHeader := r.Header.Get(HeaderDestination)
	if dstHeader == "" {
		return "", errors.Wrap(errInvalidValue, "destination header is empty")
	}
	dstURL, err := url.ParseRequestURI(dstHeader)
	if err != nil {
		return "", errors.Wrap(errInvalidValue, err.Error())
	}

	baseURI := r.Context().Value(ctxKeyBaseURI).(string)
	// TODO check if path is on same storage, return 502 on problems, see https://tools.ietf.org/html/rfc4918#section-9.9.4
	// Strip the base URI from the destination. The destination might contain redirection prefixes which need to be handled
	destination := strings.TrimPrefix(dstURL.Path, baseURI)

	// If the destination is in a spaces format, we replace with the space path
	dstSpaceID, dstRelPath := router.ShiftPath(destination)
	_, spaceRoot, ok := spaces.DecodeStorageSpaceID(dstSpaceID)
	if ok && ns != "/public" {
		destination = path.Join(spaceRoot, dstRelPath)
	} else {
		// If it is non-spaces, we join the namespace
		destination = path.Join(ns, destination)
	}

	return destination, nil
}

// replaceAllStringSubmatchFunc is taken from 'Go: Replace String with Regular Expression Callback'
// see: https://elliotchance.medium.com/go-replace-string-with-regular-expression-callback-f89948bad0bb
func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string {
	result := ""
	lastIndex := 0
	for _, v := range re.FindAllStringSubmatchIndex(str, -1) {
		groups := []string{}
		for i := 0; i < len(v); i += 2 {
			groups = append(groups, str[v[i]:v[i+1]])
		}
		result += str[lastIndex:v[0]] + repl(groups)
		lastIndex = v[1]
	}
	return result + str[lastIndex:]
}

var hrefre = regexp.MustCompile(`([^A-Za-z0-9_\-.~()/:@!$])`)

// encodePath encodes the path of a url.
//
// slashes (/) are treated as path-separators.
// ported from https://github.com/sabre-io/http/blob/bb27d1a8c92217b34e778ee09dcf79d9a2936e84/lib/functions.php#L369-L379
func encodePath(path string) string {
	return replaceAllStringSubmatchFunc(hrefre, path, func(groups []string) string {
		b := groups[1]
		var sb strings.Builder
		for i := 0; i < len(b); i++ {
			sb.WriteString(fmt.Sprintf("%%%x", b[i]))
		}
		return sb.String()
	})
}

func (s *svc) lookUpStorageSpaceReference(ctx context.Context, spaceID string, relativePath string) (*storageProvider.Reference, *rpc.Status, error) {
	// Get the getway client
	gatewayClient, err := s.getClient()
	if err != nil {
		return nil, nil, err
	}

	// retrieve a specific storage space
	lSSReq := &storageProvider.ListStorageSpacesRequest{
		Filters: []*storageProvider.ListStorageSpacesRequest_Filter{
			{
				Type: storageProvider.ListStorageSpacesRequest_Filter_TYPE_ID,
				Term: &storageProvider.ListStorageSpacesRequest_Filter_Id{
					Id: &storageProvider.StorageSpaceId{
						OpaqueId: spaceID,
					},
				},
			},
		},
	}

	lSSRes, err := gatewayClient.ListStorageSpaces(ctx, lSSReq)
	if err != nil || lSSRes.Status.Code != rpc.Code_CODE_OK {
		return nil, lSSRes.Status, err
	}

	if len(lSSRes.StorageSpaces) != 1 {
		return nil, nil, fmt.Errorf("unexpected number of spaces")
	}
	space := lSSRes.StorageSpaces[0]

	return &storageProvider.Reference{
		ResourceId: space.Root,
		Path:       utils.MakeRelativePath(relativePath),
	}, lSSRes.Status, nil
}

func requestWasMadeToResourceId(ctx context.Context, fn string) (ref *provider.Reference, ok bool) {
	if opaqueId := ctx.Value(ctxResourceOpaqueId); opaqueId != nil {
		storageId := ctx.Value(ctxStorageId)
		if storageId != nil {
			ref := &provider.Reference{
				// We make the path relative
				Path: path.Join(".", fn),
				ResourceId: &provider.ResourceId{
					StorageId: storageId.(string),
					OpaqueId:  opaqueId.(string),
				},
			}
			return ref, true
		}
	}
	return nil, false
}
