// Avatar handlers — upload, replace and clear avatars for users and
// organizations.
//
// Strategy: server receives a multipart upload, validates size + mime,
// stores the image in S3 under a deterministic key, marks the object
// public-readable and saves the public URL on the user/org row.
//
// Constants:
//
//   - max size: 2 MiB. Anything larger gets a 400.
//   - accepted MIME: image/png, image/jpeg (see allowedAvatarMIME).
//   - object key: avatars/{kind}/{id}-{epoch_ms}-{nonce}.{ext}
//
// The epoch suffix forces cache busting on replacement so the
// browser doesn't keep showing the old avatar at the same URL (the
// objects are served immutable, so a reused key would never refresh).
// The previous object is deleted best-effort once the row points at
// the new one, so replacing or removing an avatar doesn't leak blobs.

package handler

import (
	"bytes"
	"context"
	"fmt"
	"image"
	_ "image/jpeg"
	_ "image/png"
	"io"
	"net/http"
	"path"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"

	"github.com/warmbly/warmbly/internal/api/middleware"
	"github.com/warmbly/warmbly/internal/errx"
	"github.com/warmbly/warmbly/internal/infrastructure/storage"
	"github.com/warmbly/warmbly/internal/models"
	"github.com/warmbly/warmbly/internal/repository"
)

const (
	avatarMaxBytes     int64 = 2 * 1024 * 1024
	avatarMaxDimension       = 1024 // px — reject anything bigger so a phone-camera dump doesn't sneak through

	userAvatarKeyPrefix = "avatars/users/"
	orgAvatarKeyPrefix  = "avatars/organizations/"
)

// Intentionally narrow allowlist: only PNG and JPEG. WebP, GIF and
// SVG are excluded because:
//
//   - SVG can carry script payloads served from the same origin.
//   - GIF historically has decoder CVEs and we don't need motion in
//     an avatar.
//   - WebP has had several Chrome decoder CVEs (2023's heap overflow
//     being the loudest) and it's not worth the surface area when
//     PNG/JPEG cover the same use cases.
//
// The client-side resizer always re-encodes to JPEG anyway, so this
// list constrains what survives a bypass of the JS path.
var allowedAvatarMIME = map[string]string{
	"image/png":  ".png",
	"image/jpg":  ".jpg",
	"image/jpeg": ".jpg",
}

// UploadUserAvatar — POST /me/avatar (multipart, field "file")
func (h *Handler) UploadUserAvatar(c *gin.Context) {
	userIDStr := middleware.GetUserID(c)
	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errx.Handle(c, errx.ErrAuth)
		return
	}

	bytesRead, mime, ext, xerr := readAvatarUpload(c)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}

	ctx := c.Request.Context()
	previous := h.currentUserAvatarURL(ctx, userID)

	key := avatarObjectKey(userAvatarKeyPrefix, userID, ext)
	url, xerr := putPublicObject(ctx, h.Storage, key, bytesRead, mime)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}

	// Through the service, not the repo: /auth/me is served from a cached
	// copy, and only the service drops it.
	if xerr := h.UserService.UpdateAvatar(ctx, userID, &url); xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	h.deleteAvatarObject(ctx, previous, userAvatarKeyPrefix, key)

	h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityUser, &userID, nil, map[string]string{"field": "avatar_url"})

	c.JSON(http.StatusOK, gin.H{"avatar_url": url})
}

// DeleteUserAvatar — DELETE /me/avatar
func (h *Handler) DeleteUserAvatar(c *gin.Context) {
	userIDStr := middleware.GetUserID(c)
	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errx.Handle(c, errx.ErrAuth)
		return
	}

	ctx := c.Request.Context()
	previous := h.currentUserAvatarURL(ctx, userID)

	if xerr := h.UserService.UpdateAvatar(ctx, userID, nil); xerr != nil {
		errx.Handle(c, xerr)
		return
	}
	h.deleteAvatarObject(ctx, previous, userAvatarKeyPrefix, "")

	h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityUser, &userID, nil, map[string]string{"field": "avatar_url", "value": "cleared"})

	c.Status(http.StatusNoContent)
}

// UploadOrganizationAvatar — POST /organization/avatar (owner only)
func (h *Handler) UploadOrganizationAvatar(c *gin.Context) {
	userIDStr := middleware.GetUserID(c)
	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errx.Handle(c, errx.ErrAuth)
		return
	}
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.Handle(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}

	// Only the owner can change the workspace's avatar — covers the
	// same trust boundary as billing.
	if xerr := h.requireOrgOwner(c, *orgID, userID); xerr != nil {
		errx.Handle(c, xerr)
		return
	}

	bytesRead, mime, ext, xerr := readAvatarUpload(c)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}

	ctx := c.Request.Context()
	previous := h.currentOrgAvatarURL(ctx, *orgID)

	key := avatarObjectKey(orgAvatarKeyPrefix, *orgID, ext)
	url, xerr := putPublicObject(ctx, h.Storage, key, bytesRead, mime)
	if xerr != nil {
		errx.Handle(c, xerr)
		return
	}

	if err := h.OrgRepo.UpdateAvatar(ctx, *orgID, &url); err != nil {
		errx.Handle(c, errx.InternalError())
		return
	}
	h.deleteAvatarObject(ctx, previous, orgAvatarKeyPrefix, key)

	h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityOrganization, orgID, nil, map[string]string{"field": "avatar_url"})

	c.JSON(http.StatusOK, gin.H{"avatar_url": url})
}

// DeleteOrganizationAvatar — DELETE /organization/avatar (owner only)
func (h *Handler) DeleteOrganizationAvatar(c *gin.Context) {
	userIDStr := middleware.GetUserID(c)
	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		errx.Handle(c, errx.ErrAuth)
		return
	}
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.Handle(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	if xerr := h.requireOrgOwner(c, *orgID, userID); xerr != nil {
		errx.Handle(c, xerr)
		return
	}

	ctx := c.Request.Context()
	previous := h.currentOrgAvatarURL(ctx, *orgID)

	if err := h.OrgRepo.UpdateAvatar(ctx, *orgID, nil); err != nil {
		errx.Handle(c, errx.InternalError())
		return
	}
	h.deleteAvatarObject(ctx, previous, orgAvatarKeyPrefix, "")

	// Audited like the upload so teammates' org switcher refreshes live.
	h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityOrganization, orgID, nil, map[string]string{"field": "avatar_url", "value": "cleared"})

	c.Status(http.StatusNoContent)
}

// avatarObjectKey builds a key that is unique per upload: the epoch keeps keys
// sortable, the random nonce keeps two uploads in the same millisecond from
// sharing an immutably cached URL.
func avatarObjectKey(prefix string, id uuid.UUID, ext string) string {
	nonce := strings.ReplaceAll(uuid.NewString(), "-", "")[:12]
	return fmt.Sprintf("%s%s-%d-%s%s", prefix, id.String(), time.Now().UnixMilli(), nonce, ext)
}

// currentUserAvatarURL returns the avatar URL stored on the user row, or ""
// when there is none or the lookup fails (cleanup is best-effort).
func (h *Handler) currentUserAvatarURL(ctx context.Context, userID uuid.UUID) string {
	u, xerr := h.UserService.GetUser(ctx, userID)
	if xerr != nil || u == nil || u.AvatarURL == nil {
		return ""
	}
	return *u.AvatarURL
}

// currentOrgAvatarURL is the organization counterpart of currentUserAvatarURL.
func (h *Handler) currentOrgAvatarURL(ctx context.Context, orgID uuid.UUID) string {
	org, xerr := h.OrganizationService.Get(ctx, orgID)
	if xerr != nil || org == nil || org.AvatarURL == nil {
		return ""
	}
	return *org.AvatarURL
}

// deleteAvatarObject removes the object behind a previous avatar URL once the
// row no longer points at it. Only keys under our own prefix are touched, so
// an external URL (an OAuth profile picture, say) is left alone, and keepKey
// guards the freshly written object. Failures are ignored: an orphaned blob
// is harmless, a failed request after a successful update is not.
func (h *Handler) deleteAvatarObject(ctx context.Context, previousURL, prefix, keepKey string) {
	if h.Storage == nil || previousURL == "" {
		return
	}
	key := avatarKeyFromURL(previousURL, prefix)
	if key == "" || key == keepKey {
		return
	}
	_ = h.Storage.Delete(ctx, key)
}

// avatarKeyFromURL recovers the object key from a public avatar URL. Both
// storage backends build the URL differently, but the key always starts with
// the known prefix, so that is what is looked for.
func avatarKeyFromURL(url, prefix string) string {
	idx := strings.Index(url, prefix)
	if idx < 0 {
		return ""
	}
	key := url[idx:]
	if q := strings.IndexAny(key, "?#"); q >= 0 {
		key = key[:q]
	}
	if key == prefix || strings.Contains(key, "..") {
		return ""
	}
	return key
}

func (h *Handler) requireOrgOwner(c *gin.Context, orgID, userID uuid.UUID) *errx.Error {
	m, err := h.OrgRepo.GetMember(c.Request.Context(), orgID, userID)
	if err != nil || m == nil {
		return errx.ErrForbidden
	}
	if !strings.EqualFold(m.Role, string(models.RoleOwner)) {
		return errx.ErrForbidden
	}
	return nil
}

// readAvatarUpload pulls the "file" field out of the request, enforces
// the size cap, sniffs the mime, and returns the bytes ready for S3.
func readAvatarUpload(c *gin.Context) ([]byte, string, string, *errx.Error) {
	// Cap the request body before parsing so a 50MB upload doesn't
	// pin a gin worker.
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, avatarMaxBytes+1024)

	fh, err := c.FormFile("file")
	if err != nil {
		return nil, "", "", errx.New(errx.BadRequest, "file is required")
	}
	if fh.Size > avatarMaxBytes {
		return nil, "", "", errx.New(errx.BadRequest, "avatar must be smaller than 2 MB")
	}

	src, err := fh.Open()
	if err != nil {
		return nil, "", "", errx.InternalError()
	}
	defer src.Close()

	buf := &bytes.Buffer{}
	if _, err := io.Copy(buf, src); err != nil {
		return nil, "", "", errx.InternalError()
	}
	body := buf.Bytes()

	// Trust the client-declared content type only if it's on the
	// allowlist. http.DetectContentType is a stronger signal, so
	// prefer that if it disagrees.
	declared := strings.ToLower(fh.Header.Get("Content-Type"))
	sniffed := http.DetectContentType(body)
	mime := declared
	if _, ok := allowedAvatarMIME[sniffed]; ok {
		mime = sniffed
	}
	ext, ok := allowedAvatarMIME[mime]
	if !ok {
		return nil, "", "", errx.New(errx.BadRequest, "avatar must be a PNG or JPG")
	}
	// Fallback ext from the filename when mime sniff is ambiguous.
	if ext == "" {
		ext = strings.ToLower(path.Ext(fh.Filename))
		if ext == "" {
			ext = ".png"
		}
	}

	// Dimension cap. We expect the client to resize before upload —
	// this is a backstop against a raw camera dump or a bypass of the
	// JS resizer.
	cfg, _, err := image.DecodeConfig(bytes.NewReader(body))
	if err != nil {
		return nil, "", "", errx.New(errx.BadRequest, "image could not be parsed")
	}
	if cfg.Width > avatarMaxDimension || cfg.Height > avatarMaxDimension {
		return nil, "", "", errx.New(
			errx.BadRequest,
			fmt.Sprintf("avatar dimensions must be %dpx or smaller — please resize before uploading", avatarMaxDimension),
		)
	}
	return body, mime, ext, nil
}

func putPublicObject(ctx context.Context, store storage.Store, key string, body []byte, mime string) (string, *errx.Error) {
	if store == nil {
		return "", errx.New(errx.ServiceUnavailable, "object storage not configured")
	}
	// Public-read URL with long-lived cache. The backend chooses the right
	// semantics per store (S3 sets an ACL + s3 URL; filesystem writes and
	// serves via /public), so callers stay backend-agnostic.
	url, err := store.PutPublic(ctx, key, bytes.NewReader(body), mime)
	if err != nil {
		return "", errx.InternalError()
	}
	return url, nil
}

// Imports below are referenced so the file compiles cleanly even if
// future refactors remove specific dependencies above.
var _ repository.UserRepository = (repository.UserRepository)(nil)
