package server

import (
	"context"
	"errors"
	"net/http"
	"time"

	"galaxy/backend/internal/notification"
	"galaxy/backend/internal/server/handlers"
	"galaxy/backend/internal/server/httperr"

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

// AdminNotificationsHandlers groups the admin-side notification handlers
// under `/api/v1/admin/notifications/*`. The wiring connects real bodies
// backed by `*notification.Service`; tests that supply a nil service
// fall back to the Stage-3 placeholder body so the contract test
// continues to validate the OpenAPI envelope without booting Postgres.
type AdminNotificationsHandlers struct {
	svc *notification.Service
	logger *zap.Logger
}

// NewAdminNotificationsHandlers constructs the handler set. svc may be
// nil — in that case every handler returns 501 not_implemented,
// matching the pre-Stage-5.7 placeholder. logger may also be nil;
// zap.NewNop is used in that case.
func NewAdminNotificationsHandlers(svc *notification.Service, logger *zap.Logger) *AdminNotificationsHandlers {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &AdminNotificationsHandlers{svc: svc, logger: logger.Named("http.admin.notifications")}
}

// List handles GET /api/v1/admin/notifications.
func (h *AdminNotificationsHandlers) List() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("adminNotificationsList")
	}
	return func(c *gin.Context) {
		page := parsePositiveQueryInt(c.Query("page"), 1)
		pageSize := parsePositiveQueryInt(c.Query("page_size"), 50)
		ctx := c.Request.Context()
		out, err := h.svc.AdminListNotifications(ctx, page, pageSize)
		if err != nil {
			respondNotificationError(c, h.logger, "admin notifications list", ctx, err)
			return
		}
		c.JSON(http.StatusOK, notificationListToWire(out))
	}
}

// Get handles GET /api/v1/admin/notifications/{notification_id}.
func (h *AdminNotificationsHandlers) Get() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("adminNotificationsGet")
	}
	return func(c *gin.Context) {
		id, ok := parseNotificationIDParam(c)
		if !ok {
			return
		}
		ctx := c.Request.Context()
		n, err := h.svc.AdminGetNotification(ctx, id)
		if err != nil {
			respondNotificationError(c, h.logger, "admin notifications get", ctx, err)
			return
		}
		c.JSON(http.StatusOK, notificationToWire(n))
	}
}

// ListDeadLetters handles GET /api/v1/admin/notifications/dead-letters.
func (h *AdminNotificationsHandlers) ListDeadLetters() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("adminNotificationsListDeadLetters")
	}
	return func(c *gin.Context) {
		page := parsePositiveQueryInt(c.Query("page"), 1)
		pageSize := parsePositiveQueryInt(c.Query("page_size"), 50)
		ctx := c.Request.Context()
		out, err := h.svc.AdminListDeadLetters(ctx, page, pageSize)
		if err != nil {
			respondNotificationError(c, h.logger, "admin notifications list dead-letters", ctx, err)
			return
		}
		c.JSON(http.StatusOK, notificationDeadLetterListToWire(out))
	}
}

// ListMalformed handles GET /api/v1/admin/notifications/malformed.
func (h *AdminNotificationsHandlers) ListMalformed() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("adminNotificationsListMalformed")
	}
	return func(c *gin.Context) {
		page := parsePositiveQueryInt(c.Query("page"), 1)
		pageSize := parsePositiveQueryInt(c.Query("page_size"), 50)
		ctx := c.Request.Context()
		out, err := h.svc.AdminListMalformed(ctx, page, pageSize)
		if err != nil {
			respondNotificationError(c, h.logger, "admin notifications list malformed", ctx, err)
			return
		}
		c.JSON(http.StatusOK, notificationMalformedListToWire(out))
	}
}

// parseNotificationIDParam reads `notification_id` from the path. On
// invalid input it writes the standard 400 envelope and returns
// (uuid.Nil, false).
func parseNotificationIDParam(c *gin.Context) (uuid.UUID, bool) {
	parsed, err := uuid.Parse(c.Param("notification_id"))
	if err != nil {
		httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "notification_id must be a valid UUID")
		return uuid.Nil, false
	}
	return parsed, true
}

// respondNotificationError translates the notification-domain sentinels
// to HTTP. Any other error is logged and surfaced as 500 internal_error.
func respondNotificationError(c *gin.Context, logger *zap.Logger, op string, ctx context.Context, err error) {
	switch {
	case errors.Is(err, notification.ErrNotificationNotFound):
		httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "notification not found")
	case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
		httperr.Abort(c, http.StatusServiceUnavailable, httperr.CodeServiceUnavailable, "request cancelled")
	default:
		logger.Error(op+" failed", zap.Error(err))
		httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "internal server error")
	}
	_ = ctx
}

// Wire DTOs mirror the OpenAPI schemas in `backend/openapi.yaml`.

type notificationWire struct {
	NotificationID string `json:"notification_id"`
	Kind string `json:"kind"`
	IdempotencyKey string `json:"idempotency_key"`
	UserID string `json:"user_id,omitempty"`
	Payload map[string]any `json:"payload,omitempty"`
	CreatedAt string `json:"created_at"`
}

type notificationListWire struct {
	Items []notificationWire `json:"items"`
	Page int `json:"page"`
	PageSize int `json:"page_size"`
	Total int64 `json:"total"`
}

type notificationDeadLetterWire struct {
	DeadLetterID string `json:"dead_letter_id"`
	NotificationID string `json:"notification_id"`
	ArchivedAt string `json:"archived_at"`
	Reason string `json:"reason,omitempty"`
}

type notificationDeadLetterListWire struct {
	Items []notificationDeadLetterWire `json:"items"`
	Page int `json:"page"`
	PageSize int `json:"page_size"`
	Total int64 `json:"total"`
}

type notificationMalformedWire struct {
	ID string `json:"id"`
	ReceivedAt string `json:"received_at"`
	Payload map[string]any `json:"payload,omitempty"`
	Reason string `json:"reason,omitempty"`
}

type notificationMalformedListWire struct {
	Items []notificationMalformedWire `json:"items"`
	Page int `json:"page"`
	PageSize int `json:"page_size"`
	Total int64 `json:"total"`
}

func notificationToWire(n notification.Notification) notificationWire {
	out := notificationWire{
		NotificationID: n.NotificationID.String(),
		Kind: n.Kind,
		IdempotencyKey: n.IdempotencyKey,
		Payload: n.Payload,
		CreatedAt: n.CreatedAt.UTC().Format(time.RFC3339Nano),
	}
	if n.UserID != nil {
		out.UserID = n.UserID.String()
	}
	return out
}

func notificationListToWire(p notification.AdminListNotificationsPage) notificationListWire {
	items := make([]notificationWire, 0, len(p.Items))
	for _, n := range p.Items {
		items = append(items, notificationToWire(n))
	}
	return notificationListWire{
		Items: items,
		Page: p.Page,
		PageSize: p.PageSize,
		Total: p.Total,
	}
}

func notificationDeadLetterToWire(dl notification.DeadLetter) notificationDeadLetterWire {
	return notificationDeadLetterWire{
		DeadLetterID: dl.DeadLetterID.String(),
		NotificationID: dl.NotificationID.String(),
		ArchivedAt: dl.ArchivedAt.UTC().Format(time.RFC3339Nano),
		Reason: dl.Reason,
	}
}

func notificationDeadLetterListToWire(p notification.AdminListDeadLettersPage) notificationDeadLetterListWire {
	items := make([]notificationDeadLetterWire, 0, len(p.Items))
	for _, dl := range p.Items {
		items = append(items, notificationDeadLetterToWire(dl))
	}
	return notificationDeadLetterListWire{
		Items: items,
		Page: p.Page,
		PageSize: p.PageSize,
		Total: p.Total,
	}
}

func notificationMalformedToWire(m notification.MalformedIntent) notificationMalformedWire {
	return notificationMalformedWire{
		ID: m.ID.String(),
		ReceivedAt: m.ReceivedAt.UTC().Format(time.RFC3339Nano),
		Payload: m.Payload,
		Reason: m.Reason,
	}
}

func notificationMalformedListToWire(p notification.AdminListMalformedPage) notificationMalformedListWire {
	items := make([]notificationMalformedWire, 0, len(p.Items))
	for _, m := range p.Items {
		items = append(items, notificationMalformedToWire(m))
	}
	return notificationMalformedListWire{
		Items: items,
		Page: p.Page,
		PageSize: p.PageSize,
		Total: p.Total,
	}
}
