package webdav

import (
	"net/http"
	"net/url"

	"codeberg.org/shroff/phylum/server/internal/core"
	"github.com/google/uuid"
)

func (h *Handler) handleCopyMove(_ http.ResponseWriter, r *http.Request) (status int, err error) {
	hdr := r.Header.Get("Destination")
	if hdr == "" {
		return http.StatusBadRequest, errInvalidDestination
	}
	u, err := url.Parse(hdr)
	if err != nil {
		return http.StatusBadRequest, errInvalidDestination
	}
	if u.Host != "" && u.Host != r.Host {
		return http.StatusBadGateway, errInvalidDestination
	}

	src, status, err := h.stripPrefix(r.URL.Path)
	if err != nil {
		return status, err
	}

	dst, status, err := h.stripPrefix(u.Path)
	if err != nil {
		return status, err
	}

	if dst == "" {
		return http.StatusBadGateway, errInvalidDestination
	}
	if dst == src {
		return http.StatusForbidden, errDestinationEqualsSource
	}

	depth := infiniteDepth
	if hdr := r.Header.Get("Depth"); hdr != "" {
		depth = parseDepth(hdr)
	}

	if r.Method == "COPY" {
		// Section 7.5.1 says that a COPY only needs to lock the destination,
		// not both destination and source. Strictly speaking, this is racy,
		// even though a COPY doesn't modify the source, if a concurrent
		// operation modifies the source. However, the litmus test explicitly
		// checks that COPYing a locked-by-another source is OK.
		release, status, err := h.confirmLocks(r, "", dst)
		if err != nil {
			return status, err
		}
		defer release()

		// Section 9.8.3 says that "The COPY method on a collection without a Depth
		// header must act as if a Depth header with value "infinity" was included".
		if depth != 0 && depth != infiniteDepth {
			// Section 9.8.3 says that "A client may submit a Depth header on a
			// COPY on a collection with a value of "0" or "infinity"."
			return http.StatusBadRequest, errInvalidDepth
		}
		return copyFiles(h.FileSystem, src, dst, r.Header.Get("Overwrite") != "F", depth)
	}

	release, status, err := h.confirmLocks(r, src, dst)
	if err != nil {
		return status, err
	}
	defer release()

	// Section 9.9.2 says that "The MOVE method on a collection must act as if
	// a "Depth: infinity" header was used on it. A client must not submit a
	// Depth header on a MOVE on a collection with any value but "infinity"."
	if depth != infiniteDepth {
		return http.StatusBadRequest, errInvalidDepth
	}
	return moveFiles(h.FileSystem, src, dst, r.Header.Get("Overwrite") == "T")
}

// moveFiles moves files and/or directories from src to dst.
//
// See section 9.9.4 for when various HTTP status codes apply.
func moveFiles(f FileSystem, srcPath, dstPath string, overwrite bool) (status int, err error) {
	if src, err := f.ResourceByPath(srcPath); err != nil {
		if e, ok := err.(*core.Error); ok {
			return e.Status, e
		}
		return http.StatusInternalServerError, err
	} else {
		var conflictResolution core.ResourceBindConflictResolution = core.ResourceBindConflictResolutionError
		if overwrite {
			conflictResolution = core.ResourceBindConflictResolutionDelete
		}
		if _, deleted, err := f.Move(src, dstPath, conflictResolution); err != nil {
			if e, ok := err.(*core.Error); ok {
				return e.Status, e
			}
			return http.StatusForbidden, err
		} else {
			status = http.StatusCreated
			if deleted {
				status = http.StatusNoContent
			}
			return status, nil
		}
	}
}

// copyFiles copies files and/or directories from src to dst.
//
// See section 9.8.5 for when various HTTP status codes apply.
func copyFiles(f FileSystem, srcPath, dstPath string, overwrite bool, depth int) (status int, err error) {
	if src, err := f.ResourceByPath(srcPath); err != nil {
		if e, ok := err.(*core.Error); ok {
			return e.Status, e
		}
		return http.StatusInternalServerError, err
	} else {
		id, _ := uuid.NewV7()
		var conflictResolution core.ResourceBindConflictResolution = core.ResourceBindConflictResolutionError
		if overwrite {
			conflictResolution = core.ResourceBindConflictResolutionOverwrite
		}
		if _, deleted, err := f.Copy(src, dstPath, id, depth != 0, conflictResolution); err != nil {
			if e, ok := err.(*core.Error); ok {
				return e.Status, e
			}
			return http.StatusForbidden, err
		} else {
			status = http.StatusCreated
			if deleted {
				status = http.StatusNoContent
			}
			return status, nil
		}
	}
}
