package handler

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"path"
	"strings"

	"nf-repo/internal/model"
	"nf-repo/internal/opt"
	"nf-repo/internal/tool/rerr"
	"nf-repo/internal/verify"

	"github.com/loveuer/nf"
	"github.com/loveuer/nf/nft/log"
)

func handleBlobs(c *nf.Ctx) error {
	elem := strings.Split(c.Path(), "/")
	elem = elem[1:]
	if elem[len(elem)-1] == "" {
		elem = elem[:len(elem)-1]
	}

	// Must have a path of form /v2/{name}/blobs/{upload,sha256:}
	if len(elem) < 4 {
		return rerr.Error(c, &rerr.RepositoryError{
			Status:  http.StatusBadRequest,
			Code:    "NAME_INVALID",
			Message: "blobs must be attached to a repo",
		})
	}

	target := elem[len(elem)-1]
	service := elem[len(elem)-2]
	digest := c.Query("digest")
	contentRange := c.Get("Content-Range")
	rangeHeader := c.Get("Range")
	repo := c.Request.URL.Host + path.Join(elem[1:len(elem)-2]...)

	log.Debug("handleBlob: path = %s, method = %s, target = %s, service = %s, repo = %s, digest = %s",
		c.Path(), c.Method(), target, service, repo, digest)

	switch c.Method() {
	case http.MethodHead:
		h, err := model.NewHash(target)
		if err != nil {
			return rerr.Error(c, &rerr.RepositoryError{
				Status:  http.StatusBadRequest,
				Code:    "NAME_INVALID",
				Message: "invalid digest",
			})
		}

		var size int64

		size, err = b.blobHandler.Stat(c.Request.Context(), repo, h)
		if errors.Is(err, opt.ErrNotFound) {
			return rerr.Error(c, rerr.ErrBlobUnknown)
		} else if err != nil {
			var re model.RedirectError
			if errors.As(err, &re) {
				http.Redirect(c.Writer, c.Request, re.Location, re.Code)
				return nil
			}
			return rerr.Error(c, rerr.ErrInternal(err))
		}

		c.Set("Content-Length", fmt.Sprint(size))
		c.Set("Docker-Content-Digest", h.String())
		return c.SendStatus(http.StatusOK)

	case http.MethodGet:
		h, err := model.NewHash(target)
		if err != nil {
			return rerr.Error(c, &rerr.RepositoryError{
				Status:  http.StatusBadRequest,
				Code:    "NAME_INVALID",
				Message: "invalid digest",
			})
		}

		var size int64
		var r io.Reader

		size, err = b.blobHandler.Stat(c.Request.Context(), repo, h)
		if errors.Is(err, opt.ErrNotFound) {
			log.Error("handleBlobs: mirror registry get repo not found, repo = %s", repo)
			return rerr.Error(c, rerr.ErrBlobUnknown)
		} else if err != nil {
			var re model.RedirectError
			if errors.As(err, &re) {
				http.Redirect(c.Writer, c.Request, re.Location, re.Code)
				return nil
			}

			return rerr.Error(c, rerr.ErrInternal(err))
		}

		rc, err := b.blobHandler.Get(c.Request.Context(), repo, h)
		if errors.Is(err, opt.ErrNotFound) {
			return rerr.Error(c, rerr.ErrBlobUnknown)
		} else if err != nil {
			var re model.RedirectError
			if errors.As(err, &re) {
				http.Redirect(c.Writer, c.Request, re.Location, re.Code)
				return nil
			}

			return rerr.Error(c, rerr.ErrInternal(err))
		}

		defer rc.Close()
		r = rc

		if rangeHeader != "" {
			start, end := int64(0), int64(0)
			if _, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); err != nil {
				return rerr.Error(c, &rerr.RepositoryError{
					Status:  http.StatusRequestedRangeNotSatisfiable,
					Code:    "BLOB_UNKNOWN",
					Message: "We don't understand your Range",
				})
			}

			n := (end + 1) - start
			if ra, ok := r.(io.ReaderAt); ok {
				if end+1 > size {
					return rerr.Error(c, &rerr.RepositoryError{
						Status:  http.StatusRequestedRangeNotSatisfiable,
						Code:    "BLOB_UNKNOWN",
						Message: fmt.Sprintf("range end %d > %d size", end+1, size),
					})
				}
				r = io.NewSectionReader(ra, start, n)
			} else {
				if _, err := io.CopyN(io.Discard, r, start); err != nil {
					return rerr.Error(c, &rerr.RepositoryError{
						Status:  http.StatusRequestedRangeNotSatisfiable,
						Code:    "BLOB_UNKNOWN",
						Message: fmt.Sprintf("Failed to discard %d bytes", start),
					})
				}

				r = io.LimitReader(r, n)
			}

			c.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size))
			c.Set("Content-Length", fmt.Sprint(n))
			c.Set("Docker-Content-Digest", h.String())
			c.Status(http.StatusPartialContent)
		} else {
			c.Set("Content-Length", fmt.Sprint(size))
			c.Set("Docker-Content-Digest", h.String())
			c.Status(http.StatusOK)
		}

		_, err = io.Copy(c.Writer, r)
		return err

	case http.MethodPost:
		// It is weird that this is "target" instead of "service", but
		// that's how the index math works out above.
		if target != "uploads" {
			return rerr.Error(c, &rerr.RepositoryError{
				Status:  http.StatusBadRequest,
				Code:    "METHOD_UNKNOWN",
				Message: fmt.Sprintf("POST to /blobs must be followed by /uploads, got %s", target),
			})
		}

		if digest != "" {
			h, err := model.NewHash(digest)
			if err != nil {
				return rerr.Error(c, rerr.ErrDigestInvalid)
			}

			vrc, err := verify.ReadCloser(c.Request.Body, c.Request.ContentLength, h)
			if err != nil {
				return rerr.Error(c, rerr.ErrInternal(err))
			}
			defer vrc.Close()

			if err = b.blobHandler.Put(c.Request.Context(), repo, h, vrc); err != nil {
				if errors.As(err, &verify.Error{}) {
					log.Info("Digest mismatch: %v", err)
					return rerr.Error(c, rerr.ErrDigestMismatch)
				}
				return rerr.Error(c, rerr.ErrInternal(err))
			}
			c.Set("Docker-Content-Digest", h.String())
			return c.SendStatus(http.StatusCreated)
		}

		id := b.uploadHandler.UploadId()

		c.Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-2]...), "blobs/uploads", id))
		c.Set("Range", "0-0")

		return c.SendStatus(http.StatusAccepted)

	case http.MethodPatch:
		if service != "uploads" {
			return rerr.Error(c, &rerr.RepositoryError{
				Status:  http.StatusBadRequest,
				Code:    "METHOD_UNKNOWN",
				Message: fmt.Sprintf("PATCH to /blobs must be followed by /uploads, got %s", service),
			})
		}

		start, end := 0, 0

		if contentRange != "" {
			if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
				return rerr.Error(c, &rerr.RepositoryError{
					Status:  http.StatusRequestedRangeNotSatisfiable,
					Code:    "BLOB_UPLOAD_UNKNOWN",
					Message: "We don't understand your Content-Range",
				})
			}

			if end != start+int(c.Request.ContentLength) {
				return rerr.Error(c, &rerr.RepositoryError{
					Status:  http.StatusRequestedRangeNotSatisfiable,
					Code:    "BLOB_UPLOAD_INVALID",
					Message: "blob upload content range mismatch content length",
				})
			}
		}

		end = start + int(c.Request.ContentLength)

		var (
			length int
			re     *rerr.RepositoryError
		)
		if length, re = b.uploadHandler.Write(c.Request.Context(), target, c.Request.Body, start, end); re != nil {
			return rerr.Error(c, re)
		}

		c.Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target))
		c.Set("Range", fmt.Sprintf("0-%d", length-1))
		return c.SendStatus(http.StatusNoContent)

	case http.MethodPut:
		if service != "uploads" {
			return rerr.Error(c, &rerr.RepositoryError{
				Status:  http.StatusBadRequest,
				Code:    "METHOD_UNKNOWN",
				Message: fmt.Sprintf("PUT to /blobs must be followed by /uploads, got %s", service),
			})
		}

		if digest == "" {
			return rerr.Error(c, &rerr.RepositoryError{
				Status:  http.StatusBadRequest,
				Code:    "DIGEST_INVALID",
				Message: "digest not specified",
			})
		}

		hash, err := model.NewHash(digest)
		if err != nil {
			return rerr.Error(c, &rerr.RepositoryError{
				Status:  http.StatusBadRequest,
				Code:    "NAME_INVALID",
				Message: "invalid digest",
			})
		}

		var re *rerr.RepositoryError
		if re = b.uploadHandler.Done(c.Request.Context(), b.blobHandler, target, c.Request.Body, int(c.Request.ContentLength), repo, hash); re != nil {
			return rerr.Error(c, re)
		}

		c.Set("Docker-Content-Digest", hash.String())
		return c.SendStatus(http.StatusCreated)

	case http.MethodDelete:
		h, err := model.NewHash(target)
		if err != nil {
			return rerr.Error(c, &rerr.RepositoryError{
				Status:  http.StatusBadRequest,
				Code:    "NAME_INVALID",
				Message: "invalid digest",
			})
		}
		if err := b.blobHandler.Delete(c.Request.Context(), repo, h); err != nil {
			return rerr.Error(c, rerr.ErrInternal(err))
		}
		return c.SendStatus(http.StatusAccepted)

	default:
		return rerr.Error(c, &rerr.RepositoryError{
			Status:  http.StatusBadRequest,
			Code:    "METHOD_UNKNOWN",
			Message: "We don't understand your method + url",
		})
	}
}