package rest

import (
	"errors"
	"io"
	"net/http"
	"strconv"
	"strings"

	"github.com/pingcap/agent-git-service/internal/rest/respond"
	"github.com/pingcap/agent-git-service/internal/rest/transform"
	"github.com/pingcap/agent-git-service/internal/service"
)

const avatarMultipartOverheadBytes int64 = 128 << 10

// UploadViewerAvatar handles PUT /api/ext/v1/user/avatar.
func (d *Deps) UploadViewerAvatar(w http.ResponseWriter, r *http.Request) {
	content, ok := readAvatarUpload(w, r)
	if !ok {
		return
	}
	viewer, err := d.Svc.GetCurrentUser(r.Context())
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	updated, err := d.Svc.SetAvatar(r.Context(), viewer.ID, content)
	if err != nil {
		respondAvatarError(r, w, err)
		return
	}
	respond.JSON(w, http.StatusOK, transform.FromContext(r.Context()).UserPrivate(updated))
}

// DeleteViewerAvatar handles DELETE /api/ext/v1/user/avatar.
func (d *Deps) DeleteViewerAvatar(w http.ResponseWriter, r *http.Request) {
	viewer, err := d.Svc.GetCurrentUser(r.Context())
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	updated, err := d.Svc.DeleteAvatar(r.Context(), viewer.ID)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusOK, transform.FromContext(r.Context()).UserPrivate(updated))
}

// UploadOrgAvatar handles PUT /api/ext/v1/orgs/{org}/avatar.
func (d *Deps) UploadOrgAvatar(w http.ResponseWriter, r *http.Request) {
	org := d.mustGetOrg(w, r)
	if org == nil || !d.requireOrgAdmin(w, r, org) {
		return
	}
	content, ok := readAvatarUpload(w, r)
	if !ok {
		return
	}
	updated, err := d.Svc.SetAvatar(r.Context(), org.ID, content)
	if err != nil {
		respondAvatarError(r, w, err)
		return
	}
	respond.JSON(w, http.StatusOK, transform.FromContext(r.Context()).User(updated))
}

// DeleteOrgAvatar handles DELETE /api/ext/v1/orgs/{org}/avatar.
func (d *Deps) DeleteOrgAvatar(w http.ResponseWriter, r *http.Request) {
	org := d.mustGetOrg(w, r)
	if org == nil || !d.requireOrgAdmin(w, r, org) {
		return
	}
	updated, err := d.Svc.DeleteAvatar(r.Context(), org.ID)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusOK, transform.FromContext(r.Context()).User(updated))
}

// GetAvatar handles GET /avatars/{login}. It is intentionally public, exactly
// like GitHub's avatar URLs. A stale versioned URL is not silently served as a
// newer image, which keeps cache-busting behavior deterministic.
func (d *Deps) GetAvatar(w http.ResponseWriter, r *http.Request) {
	avatar, err := d.Svc.GetAvatar(r.Context(), pathParam(r, "login"))
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	if raw := strings.TrimSpace(r.URL.Query().Get("v")); raw != "" {
		version, err := strconv.ParseUint(raw, 10, 64)
		if err != nil || version != uint64(avatar.Version) {
			respond.NotFound(w)
			return
		}
		w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
	} else {
		w.Header().Set("Cache-Control", "no-cache")
	}
	w.Header().Set("Content-Type", avatar.ContentType)
	w.Header().Set("X-Content-Type-Options", "nosniff")
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write(avatar.Content)
}

func readAvatarUpload(w http.ResponseWriter, r *http.Request) ([]byte, bool) {
	r.Body = http.MaxBytesReader(w, r.Body, service.AvatarMaxUploadBytes+avatarMultipartOverheadBytes)
	reader, err := r.MultipartReader()
	if err != nil {
		respond.Error(w, http.StatusBadRequest, "expected multipart/form-data with a file field")
		return nil, false
	}

	var content []byte
	for {
		part, err := reader.NextPart()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			if isAvatarRequestTooLarge(err) {
				respond.Error(w, http.StatusRequestEntityTooLarge, "avatar image exceeds the 1 MiB limit")
			} else {
				respond.Error(w, http.StatusBadRequest, "invalid multipart form data")
			}
			return nil, false
		}
		if part.FormName() != "file" {
			_, _ = io.Copy(io.Discard, part)
			continue
		}
		if content != nil {
			respond.Error(w, http.StatusBadRequest, "exactly one avatar file is required")
			return nil, false
		}
		bytes, err := io.ReadAll(io.LimitReader(part, service.AvatarMaxUploadBytes+1))
		if err != nil {
			if isAvatarRequestTooLarge(err) {
				respond.Error(w, http.StatusRequestEntityTooLarge, "avatar image exceeds the 1 MiB limit")
			} else {
				respond.Error(w, http.StatusBadRequest, "unable to read avatar image")
			}
			return nil, false
		}
		if len(bytes) > service.AvatarMaxUploadBytes {
			respond.Error(w, http.StatusRequestEntityTooLarge, "avatar image exceeds the 1 MiB limit")
			return nil, false
		}
		content = bytes
	}
	if content == nil {
		respond.Error(w, http.StatusBadRequest, "multipart form must include a file field")
		return nil, false
	}
	return content, true
}

func isAvatarRequestTooLarge(err error) bool {
	var maxErr *http.MaxBytesError
	return errors.As(err, &maxErr)
}

func respondAvatarError(r *http.Request, w http.ResponseWriter, err error) {
	switch {
	case errors.Is(err, service.ErrAvatarTooLarge):
		respond.Error(w, http.StatusRequestEntityTooLarge, "avatar image exceeds the 1 MiB limit")
	case errors.Is(err, service.ErrAvatarInvalid), errors.Is(err, service.ErrAvatarMediaType):
		respond.Error(w, http.StatusUnsupportedMediaType, "avatar image must be a valid PNG, JPEG, or GIF no larger than 3000px")
	default:
		respond.ServiceErrorRequest(r, w, err)
	}
}
