package server

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

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

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

// AdminMailHandlers groups the admin-side mail-outbox handlers under
// `/api/v1/admin/mail/*`. The wiring connects real bodies backed by
// `*mail.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 AdminMailHandlers struct {
	svc *mail.Service
	logger *zap.Logger
}

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

// ListDeliveries handles GET /api/v1/admin/mail/deliveries.
func (h *AdminMailHandlers) ListDeliveries() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("adminMailListDeliveries")
	}
	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.AdminListDeliveries(ctx, page, pageSize)
		if err != nil {
			respondMailError(c, h.logger, "admin mail list deliveries", ctx, err)
			return
		}
		c.JSON(http.StatusOK, mailDeliveryListToWire(out))
	}
}

// GetDelivery handles GET /api/v1/admin/mail/deliveries/{delivery_id}.
func (h *AdminMailHandlers) GetDelivery() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("adminMailGetDelivery")
	}
	return func(c *gin.Context) {
		deliveryID, ok := parseDeliveryIDParam(c)
		if !ok {
			return
		}
		ctx := c.Request.Context()
		d, err := h.svc.AdminGetDelivery(ctx, deliveryID)
		if err != nil {
			respondMailError(c, h.logger, "admin mail get delivery", ctx, err)
			return
		}
		c.JSON(http.StatusOK, mailDeliveryToWire(d))
	}
}

// ListDeliveryAttempts handles GET /api/v1/admin/mail/deliveries/{delivery_id}/attempts.
func (h *AdminMailHandlers) ListDeliveryAttempts() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("adminMailListDeliveryAttempts")
	}
	return func(c *gin.Context) {
		deliveryID, ok := parseDeliveryIDParam(c)
		if !ok {
			return
		}
		ctx := c.Request.Context()
		attempts, err := h.svc.AdminListAttempts(ctx, deliveryID)
		if err != nil {
			respondMailError(c, h.logger, "admin mail list attempts", ctx, err)
			return
		}
		c.JSON(http.StatusOK, mailAttemptListToWire(attempts))
	}
}

// ResendDelivery handles POST /api/v1/admin/mail/deliveries/{delivery_id}/resend.
func (h *AdminMailHandlers) ResendDelivery() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("adminMailResendDelivery")
	}
	return func(c *gin.Context) {
		deliveryID, ok := parseDeliveryIDParam(c)
		if !ok {
			return
		}
		ctx := c.Request.Context()
		d, err := h.svc.AdminResendDelivery(ctx, deliveryID)
		if err != nil {
			respondMailError(c, h.logger, "admin mail resend delivery", ctx, err)
			return
		}
		c.JSON(http.StatusAccepted, mailDeliveryToWire(d))
	}
}

// ListDeadLetters handles GET /api/v1/admin/mail/dead-letters.
func (h *AdminMailHandlers) ListDeadLetters() gin.HandlerFunc {
	if h.svc == nil {
		return handlers.NotImplemented("adminMailListDeadLetters")
	}
	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 {
			respondMailError(c, h.logger, "admin mail list dead-letters", ctx, err)
			return
		}
		c.JSON(http.StatusOK, mailDeadLetterListToWire(out))
	}
}

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

// respondMailError translates the mail-domain sentinels to HTTP. Any
// other error is logged and surfaced as 500 internal_error so the
// handler always emits the documented envelope.
func respondMailError(c *gin.Context, logger *zap.Logger, op string, ctx context.Context, err error) {
	switch {
	case errors.Is(err, mail.ErrDeliveryNotFound):
		httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "mail delivery not found")
	case errors.Is(err, mail.ErrResendOnSent):
		httperr.Abort(c, http.StatusConflict, httperr.CodeConflict, "delivery already sent")
	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 schemas in `backend/openapi.yaml`.

type mailDeliveryWire struct {
	DeliveryID string `json:"delivery_id"`
	TemplateID string `json:"template_id"`
	IdempotencyKey string `json:"idempotency_key,omitempty"`
	Status string `json:"status"`
	Attempts int32 `json:"attempts"`
	NextAttemptAt *string `json:"next_attempt_at,omitempty"`
	CreatedAt string `json:"created_at"`
}

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

type mailAttemptWire struct {
	AttemptID string `json:"attempt_id"`
	DeliveryID string `json:"delivery_id"`
	AttemptNo int32 `json:"attempt_no"`
	StartedAt string `json:"started_at"`
	FinishedAt *string `json:"finished_at,omitempty"`
	Outcome string `json:"outcome,omitempty"`
	Error string `json:"error,omitempty"`
}

type mailAttemptListWire struct {
	Items []mailAttemptWire `json:"items"`
}

type mailDeadLetterWire struct {
	DeadLetterID string `json:"dead_letter_id"`
	DeliveryID string `json:"delivery_id"`
	ArchivedAt string `json:"archived_at"`
	Reason string `json:"reason,omitempty"`
}

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

func mailDeliveryToWire(d mail.Delivery) mailDeliveryWire {
	out := mailDeliveryWire{
		DeliveryID: d.DeliveryID.String(),
		TemplateID: d.TemplateID,
		IdempotencyKey: d.IdempotencyKey,
		Status: d.Status,
		Attempts: d.Attempts,
		CreatedAt: d.CreatedAt.UTC().Format(time.RFC3339Nano),
	}
	if d.NextAttemptAt != nil {
		s := d.NextAttemptAt.UTC().Format(time.RFC3339Nano)
		out.NextAttemptAt = &s
	}
	return out
}

func mailDeliveryListToWire(p mail.AdminListDeliveriesPage) mailDeliveryListWire {
	items := make([]mailDeliveryWire, 0, len(p.Items))
	for _, d := range p.Items {
		items = append(items, mailDeliveryToWire(d))
	}
	return mailDeliveryListWire{
		Items: items,
		Page: p.Page,
		PageSize: p.PageSize,
		Total: p.Total,
	}
}

func mailAttemptToWire(a mail.Attempt) mailAttemptWire {
	out := mailAttemptWire{
		AttemptID: a.AttemptID.String(),
		DeliveryID: a.DeliveryID.String(),
		AttemptNo: a.AttemptNo,
		StartedAt: a.StartedAt.UTC().Format(time.RFC3339Nano),
		Outcome: a.Outcome,
		Error: a.Error,
	}
	if a.FinishedAt != nil {
		s := a.FinishedAt.UTC().Format(time.RFC3339Nano)
		out.FinishedAt = &s
	}
	return out
}

func mailAttemptListToWire(items []mail.Attempt) mailAttemptListWire {
	out := mailAttemptListWire{Items: make([]mailAttemptWire, 0, len(items))}
	for _, a := range items {
		out.Items = append(out.Items, mailAttemptToWire(a))
	}
	return out
}

func mailDeadLetterToWire(dl mail.DeadLetter) mailDeadLetterWire {
	return mailDeadLetterWire{
		DeadLetterID: dl.DeadLetterID.String(),
		DeliveryID: dl.DeliveryID.String(),
		ArchivedAt: dl.ArchivedAt.UTC().Format(time.RFC3339Nano),
		Reason: dl.Reason,
	}
}

func mailDeadLetterListToWire(p mail.AdminListDeadLettersPage) mailDeadLetterListWire {
	items := make([]mailDeadLetterWire, 0, len(p.Items))
	for _, dl := range p.Items {
		items = append(items, mailDeadLetterToWire(dl))
	}
	return mailDeadLetterListWire{
		Items: items,
		Page: p.Page,
		PageSize: p.PageSize,
		Total: p.Total,
	}
}
