package server

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

	"github.com/gin-gonic/gin"
	"golang.org/x/net/webdav"

	"github.com/photoprism/photoprism/internal/config"
	"github.com/photoprism/photoprism/internal/mutex"
	"github.com/photoprism/photoprism/internal/workers/auto"
	"github.com/photoprism/photoprism/pkg/clean"
	"github.com/photoprism/photoprism/pkg/fs"
	"github.com/photoprism/photoprism/pkg/http/header"
	"github.com/photoprism/photoprism/pkg/txt"
)

// WebDAVHandler wraps the http request handler so that it can be customized.
var WebDAVHandler = func(c *gin.Context, router *gin.RouterGroup, srv *webdav.Handler) {
	srv.ServeHTTP(c.Writer, c.Request)
}

// WebDAV handles requests to the "/originals" and "/import" endpoints.
func WebDAV(dir string, router *gin.RouterGroup, conf *config.Config) {
	if router == nil {
		log.Error("webdav: router is nil")
		return
	}

	if conf == nil {
		log.Error("webdav: conf is nil")
		return
	}

	// Native file system restricted to a specific directory.
	fileSystem := webdav.Dir(dir)
	lockSystem := mutex.WebDAV(dir)

	// Request logger function.
	loggerFunc := func(request *http.Request, err error) {
		if err != nil {
			switch request.Method {
			case MethodPut, MethodPost, MethodPatch, MethodDelete, MethodCopy, MethodMove:
				log.Errorf("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
			case MethodPropfind:
				log.Tracef("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
			default:
				log.Debugf("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
			}
		} else {
			// Determine the filename if it is an uploaded file and process custom request headers, if any.
			if fileName := WebDAVFileName(request, router, conf); fileName != "" {
				// Flag the uploaded file as favorite if the "X-Favorite" header is set to "1".
				if request.Header.Get(header.XFavorite) == "1" {
					WebDAVSetFavoriteFlag(fileName)
				}

				// Set the file modification time based on the Unix timestamp found in the "X-OC-MTime" header.
				if fileMtime := txt.Int64(request.Header.Get(header.XModTime)); fileMtime > 0 {
					WebDAVSetFileMtime(fileName, fileMtime)
				}
			}

			switch request.Method {
			case MethodPut, MethodPost, MethodPatch, MethodDelete, MethodCopy, MethodMove:
				log.Infof("webdav: %s %s", clean.Log(request.Method), clean.Log(request.URL.String()))

				if router.BasePath() == conf.BaseUri(WebDAVOriginals) {
					auto.ShouldIndex()
				} else if router.BasePath() == conf.BaseUri(WebDAVImport) {
					auto.ShouldImport()
				}
			default:
				log.Tracef("webdav: %s %s", clean.Log(request.Method), clean.Log(request.URL.String()))
			}
		}
	}

	// Create WebDAV request handler.
	srv := &webdav.Handler{
		Prefix:     router.BasePath(),
		FileSystem: fileSystem,
		LockSystem: lockSystem,
		Logger:     loggerFunc,
	}

	// Wrap handler to check quota and permissions.
	handlerFunc := func(c *gin.Context) {
		// Abort PUT, POST, PATCH, and COPY requests if there
		// is not enough free storage to upload new files.
		switch c.Request.Method {
		case MethodPut, MethodPost, MethodPatch, MethodCopy:
			if conf.FilesQuotaReached() {
				c.AbortWithStatus(http.StatusInsufficientStorage)
				return
			}
		}

		// Invoke handler callback.
		WebDAVHandler(c, router, srv)
	}

	// handleRead registers WebDAV methods used for browsing and downloading.
	handleRead := func(h func(*gin.Context)) {
		router.Handle(MethodHead, "/*path", h)
		router.Handle(MethodGet, "/*path", h)
		router.Handle(MethodOptions, "/*path", h)
		router.Handle(MethodLock, "/*path", h)
		router.Handle(MethodUnlock, "/*path", h)
		router.Handle(MethodPropfind, "/*path", h)
	}

	// handleWrite registers WebDAV methods to may modify the file system.
	handleWrite := func(h func(*gin.Context)) {
		router.Handle(MethodPut, "/*path", h)
		router.Handle(MethodPost, "/*path", h)
		router.Handle(MethodPatch, "/*path", h)
		router.Handle(MethodDelete, "/*path", h)
		router.Handle(MethodMkcol, "/*path", h)
		router.Handle(MethodCopy, "/*path", h)
		router.Handle(MethodMove, "/*path", h)
		router.Handle(MethodProppatch, "/*path", h)
	}

	// Handle supported WebDAV request methods.
	handleRead(handlerFunc)

	// Only supported with read-only mode disabled.
	if conf.ReadOnly() {
		handleWrite(func(c *gin.Context) {
			_ = c.AbortWithError(http.StatusForbidden, fmt.Errorf("forbidden in read-only mode"))
		})
	} else {
		handleWrite(handlerFunc)
	}
}

// WebDAVFileName determines the name and path of an uploaded file and returns its name if it exists.
func WebDAVFileName(request *http.Request, router *gin.RouterGroup, conf *config.Config) (fileName string) {
	// Check if this is a PUT request, as used for file uploads.
	if request.Method != MethodPut {
		return ""
	}

	basePath := router.BasePath()

	// Determine the absolute file path based on the request URL and the configuration.
	switch basePath {
	case conf.BaseUri(WebDAVOriginals):
		// Resolve the requested path safely under OriginalsPath.
		rel := strings.TrimPrefix(request.URL.Path, basePath)
		// Make relative if a leading slash remains after trimming the base.
		rel = strings.TrimLeft(rel, "/\\")
		if name, err := joinUnderBase(conf.OriginalsPath(), rel); err == nil {
			fileName = name
		} else {
			return ""
		}
	case conf.BaseUri(WebDAVImport):
		// Resolve the requested path safely under ImportPath.
		rel := strings.TrimPrefix(request.URL.Path, basePath)
		rel = strings.TrimLeft(rel, "/\\")
		if name, err := joinUnderBase(conf.ImportPath(), rel); err == nil {
			fileName = name
		} else {
			return ""
		}
	default:
		return ""
	}

	// Check if the file actually exists and return an empty string otherwise.
	if !fs.FileExists(fileName) {
		return ""
	}

	return fileName
}

// joinUnderBase joins a base directory with a relative name and ensures
// that the resulting path stays within the base directory. Absolute
// paths and Windows-style volume names are rejected.
func joinUnderBase(baseDir, rel string) (string, error) {
	if rel == "" {
		return "", fmt.Errorf("invalid path")
	}
	// Reject absolute or volume paths.
	if filepath.IsAbs(rel) || filepath.VolumeName(rel) != "" {
		return "", fmt.Errorf("invalid path: absolute or volume path not allowed")
	}
	cleaned := filepath.Clean(rel)
	// Compose destination and verify it stays inside base.
	dest := filepath.Join(baseDir, cleaned)
	base := filepath.Clean(baseDir)
	if dest != base && !strings.HasPrefix(dest, base+string(os.PathSeparator)) {
		return "", fmt.Errorf("invalid path: outside base directory")
	}
	return dest, nil
}

// WebDAVSetFavoriteFlag adds the favorite flag to files uploaded via WebDAV.
func WebDAVSetFavoriteFlag(fileName string) {
	yamlName := fs.AbsPrefix(fileName, false) + fs.ExtYml

	// Abort if YAML file already exists to avoid overwriting metadata.
	if fs.FileExists(yamlName) {
		log.Warnf("webdav: %s already exists", clean.Log(filepath.Base(yamlName)))
		return
	}

	// Make sure directory exists.
	if err := fs.MkdirAll(filepath.Dir(yamlName)); err != nil {
		log.Errorf("webdav: %s", err.Error())
		return
	}

	// Write YAML data to file.
	if err := fs.WriteFile(yamlName, []byte("Favorite: true\n"), fs.ModeConfigFile); err != nil {
		log.Errorf("webdav: %s", err.Error())
		return
	}

	// Log success.
	log.Infof("webdav: flagged %s as favorite", clean.Log(filepath.Base(fileName)))
}

// WebDAVSetFileMtime updaters the file modification time based on a Unix timestamp string.
func WebDAVSetFileMtime(fileName string, mtimeUnix int64) {
	if mtime := time.Unix(mtimeUnix, 0); mtimeUnix <= 0 || mtime.IsZero() || time.Now().Before(mtime) {
		log.Warnf("webdav: invalid mtime provided for %s", clean.Log(filepath.Base(fileName)))
	} else if mtimeErr := os.Chtimes(fileName, time.Time{}, mtime); mtimeErr != nil {
		log.Warnf("webdav: failed to set mtime for %s", clean.Log(filepath.Base(fileName)))
	} else {
		log.Infof("webdav: set mtime for %s", clean.Log(filepath.Base(fileName)))
	}
}
