package httpx

import (
	"errors"
	"net/http"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo/options"

	"bountyboard/internal/domain"
	"bountyboard/internal/store"
)

func (s *Server) handleAdminListUsers(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	filter := bson.M{}
	if search := q.Get("q"); search != "" {
		filter["$or"] = []bson.M{
			{"email": bson.M{"$regex": regexEscape(search), "$options": "i"}},
			{"name": bson.M{"$regex": regexEscape(search), "$options": "i"}},
		}
	}
	if role := q.Get("role"); role != "" {
		filter["roles."+role] = true
	}
	if cursor := q.Get("cursor"); cursor != "" {
		filter["_id"] = bson.M{"$gt": cursor}
	}
	limit := atoiDefault(q.Get("limit"), 100)
	if limit > 200 {
		limit = 200
	}
	cur, err := s.store.DB.Collection("users").Find(r.Context(), filter,
		options.Find().SetSort(bson.D{{Key: "_id", Value: 1}}).SetLimit(int64(limit)))
	if err != nil {
		s.internalError(w, r, "list users", err)
		return
	}
	users := []domain.User{}
	if err := cur.All(r.Context(), &users); err != nil {
		s.internalError(w, r, "decode users", err)
		return
	}
	next := ""
	if len(users) == limit {
		next = users[len(users)-1].ID
	}
	writeJSON(w, http.StatusOK, map[string]any{"users": users, "nextCursor": next})
}

func (s *Server) handleAdminPatchUser(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	var req struct {
		Roles      *domain.Roles `json:"roles"`
		Disabled   *bool         `json:"disabled"`
		ForceReset *bool         `json:"forceReset"`
		Name       *string       `json:"name"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}
	actor := CurrentUser(r.Context())
	target, err := s.store.UserByID(r.Context(), id)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			writeError(w, http.StatusNotFound, "not_found", "user not found")
			return
		}
		s.internalError(w, r, "load user", err)
		return
	}

	set := bson.M{}
	diff := map[string]any{}
	if req.Roles != nil {
		// An admin cannot remove their own admin flag — lockout guard.
		if target.ID == actor.ID && !req.Roles.Admin {
			writeError(w, http.StatusBadRequest, "self_demotion", "you cannot remove your own admin role")
			return
		}
		set["roles"] = *req.Roles
		diff["roles"] = *req.Roles
	}
	if req.Disabled != nil {
		if target.ID == actor.ID && *req.Disabled {
			writeError(w, http.StatusBadRequest, "self_disable", "you cannot disable your own account")
			return
		}
		set["disabled"] = *req.Disabled
		diff["disabled"] = *req.Disabled
	}
	if req.Name != nil && *req.Name != "" {
		set["name"] = *req.Name
		diff["name"] = *req.Name
	}
	if req.ForceReset != nil && *req.ForceReset {
		if target.Auth.Local == nil {
			writeError(w, http.StatusBadRequest, "no_local_auth", "user has no local password to reset")
			return
		}
		set["auth.local.mustChange"] = true
		diff["forceReset"] = true
	}
	if len(set) == 0 {
		writeError(w, http.StatusBadRequest, "empty_update", "no recognized fields")
		return
	}

	if err := store.UpdateVersioned(r.Context(), s.store.DB.Collection("users"),
		id, target.Version, bson.M{"$set": set}); err != nil {
		if errors.Is(err, store.ErrVersionConflict) {
			writeError(w, http.StatusConflict, "conflict", "user was modified concurrently; retry")
			return
		}
		s.internalError(w, r, "patch user", err)
		return
	}
	// Disabling revokes all sessions immediately.
	if req.Disabled != nil && *req.Disabled {
		if _, err := s.store.DeleteUserSessions(r.Context(), id); err != nil {
			s.log.Warn("revoke sessions of disabled user", "err", err)
		}
	}
	s.audit(r, "user.update", "user", id, diff)

	fresh, err := s.store.UserByID(r.Context(), id)
	if err != nil {
		s.internalError(w, r, "reload user", err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"user": fresh})
}

func (s *Server) handleAdminDeleteUser(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	if id == CurrentUser(r.Context()).ID {
		writeError(w, http.StatusBadRequest, "self_delete", "you cannot delete your own account")
		return
	}
	res, err := s.store.DB.Collection("users").DeleteOne(r.Context(), bson.M{"_id": id})
	if err != nil {
		s.internalError(w, r, "delete user", err)
		return
	}
	if res.DeletedCount == 0 {
		writeError(w, http.StatusNotFound, "not_found", "user not found")
		return
	}
	if _, err := s.store.DeleteUserSessions(r.Context(), id); err != nil {
		s.log.Warn("revoke sessions of deleted user", "err", err)
	}
	s.audit(r, "user.delete", "user", id, nil)
	w.WriteHeader(http.StatusNoContent)
}

// regexEscape neutralizes regex metacharacters in user-supplied search text.
func regexEscape(s string) string {
	out := make([]rune, 0, len(s))
	for _, r := range s {
		switch r {
		case '.', '+', '*', '?', '(', ')', '[', ']', '{', '}', '^', '$', '|', '\\':
			out = append(out, '\\')
		}
		out = append(out, r)
	}
	return string(out)
}
