package server

import (
	"errors"
	"net/http"

	"galaxy/backend/internal/auth"
	"galaxy/backend/internal/server/handlers"
	"galaxy/backend/internal/server/httperr"
	"galaxy/backend/internal/server/middleware/userid"
	"galaxy/backend/internal/telemetry"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"go.uber.org/zap"
)

// UserSessionsHandlers groups the user-facing session handlers under
// `/api/v1/user/sessions/*`. Authenticated callers can list their own
// active device sessions, revoke a specific one (logout from one
// device), or revoke all sessions at once (logout everywhere). Every
// mutation lands an audit row in `session_revocations` through the
// auth service. nil *auth.Service falls back to the standard 501
// placeholder.
type UserSessionsHandlers struct {
	svc    *auth.Service
	logger *zap.Logger
}

// NewUserSessionsHandlers constructs the handler set. svc may be nil
// — in that case every handler returns 501 not_implemented.
func NewUserSessionsHandlers(svc *auth.Service, logger *zap.Logger) *UserSessionsHandlers {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &UserSessionsHandlers{svc: svc, logger: logger.Named("http.user.sessions")}
}

type userSessionsListResponse struct {
	Items []deviceSessionPayload `json:"items"`
}

type userSessionsRevocationSummary struct {
	UserID       string `json:"user_id"`
	RevokedCount int    `json:"revoked_count"`
}

// List handles GET /api/v1/user/sessions.
func (h *UserSessionsHandlers) List() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("userSessionsList")
	}
	return func(c *gin.Context) {
		callerID, ok := userid.FromContext(c.Request.Context())
		if !ok {
			httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "X-User-ID header is required")
			return
		}
		sessions := h.svc.ListActiveByUser(c.Request.Context(), callerID)
		items := make([]deviceSessionPayload, 0, len(sessions))
		for _, s := range sessions {
			items = append(items, deviceSessionToWire(s))
		}
		c.JSON(http.StatusOK, userSessionsListResponse{Items: items})
	}
}

// Revoke handles POST /api/v1/user/sessions/{device_session_id}/revoke.
// The target session must belong to the caller; otherwise the handler
// returns 404 (using the same shape as a missing session) so callers
// cannot probe foreign device_session_ids.
func (h *UserSessionsHandlers) Revoke() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("userSessionsRevoke")
	}
	return func(c *gin.Context) {
		callerID, ok := userid.FromContext(c.Request.Context())
		if !ok {
			httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "X-User-ID header is required")
			return
		}
		deviceSessionID, err := uuid.Parse(c.Param("device_session_id"))
		if err != nil {
			httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "device_session_id must be a valid UUID")
			return
		}
		// Ownership check via the cache — if the target session is not
		// active and owned by the caller, surface a 404 in both
		// branches so foreign sessions are not probeable.
		cached, ok := h.svc.LookupSessionInCache(deviceSessionID)
		if !ok || cached.UserID != callerID {
			httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "device session not found")
			return
		}
		ctx := c.Request.Context()
		sess, err := h.svc.RevokeSession(ctx, deviceSessionID, auth.RevokeContext{
			ActorKind: auth.ActorKindUserSelf,
			ActorID:   callerID.String(),
		})
		if err != nil {
			if errors.Is(err, auth.ErrSessionNotFound) {
				httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "device session not found")
				return
			}
			h.logger.Error("user sessions revoke failed",
				append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))...,
			)
			httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "service error")
			return
		}
		c.JSON(http.StatusOK, deviceSessionToWire(sess))
	}
}

// RevokeAll handles POST /api/v1/user/sessions/revoke-all.
func (h *UserSessionsHandlers) RevokeAll() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("userSessionsRevokeAll")
	}
	return func(c *gin.Context) {
		callerID, ok := userid.FromContext(c.Request.Context())
		if !ok {
			httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "X-User-ID header is required")
			return
		}
		ctx := c.Request.Context()
		revoked, err := h.svc.RevokeAllForUser(ctx, callerID, auth.RevokeContext{
			ActorKind: auth.ActorKindUserSelf,
			ActorID:   callerID.String(),
		})
		if err != nil {
			h.logger.Error("user sessions revoke-all failed",
				append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))...,
			)
			httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "service error")
			return
		}
		c.JSON(http.StatusOK, userSessionsRevocationSummary{
			UserID:       callerID.String(),
			RevokedCount: len(revoked),
		})
	}
}
