package server

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

	"galaxy/backend/internal/runtime"
	"galaxy/backend/internal/server/handlers"
	"galaxy/backend/internal/server/httperr"
	"galaxy/backend/internal/telemetry"

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

// AdminRuntimesHandlers groups the admin-side runtime handlers under
// `/api/v1/admin/runtimes/*`. The implementation swaps the placeholder bodies
// for real `*runtime.Service` calls; tests that omit the service fall
// back to 501.
type AdminRuntimesHandlers struct {
	svc *runtime.Service
	logger *zap.Logger
}

// NewAdminRuntimesHandlers constructs the handler set. svc may be
// nil — placeholders are returned in that case.
func NewAdminRuntimesHandlers(svc *runtime.Service, logger *zap.Logger) *AdminRuntimesHandlers {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &AdminRuntimesHandlers{svc: svc, logger: logger.Named("http.admin.runtimes")}
}

// Get handles GET /api/v1/admin/runtimes/{game_id}.
func (h *AdminRuntimesHandlers) Get() gin.HandlerFunc {
	if h == nil || h.svc == nil {
		return handlers.NotImplemented("adminRuntimesGet")
	}
	return func(c *gin.Context) {
		gameID, ok := parseGameIDParam(c)
		if !ok {
			return
		}
		ctx := c.Request.Context()
		rec, err := h.svc.GetRuntime(ctx, gameID)
		if err != nil {
			respondRuntimeError(c, h.logger, "admin runtimes get", ctx, err)
			return
		}
		c.JSON(http.StatusOK, runtimeRecordToWire(rec))
	}
}

// Restart handles POST /api/v1/admin/runtimes/{game_id}/restart.
func (h *AdminRuntimesHandlers) Restart() gin.HandlerFunc {
	if h == nil || h.svc == nil {
		return handlers.NotImplemented("adminRuntimesRestart")
	}
	return func(c *gin.Context) {
		gameID, ok := parseGameIDParam(c)
		if !ok {
			return
		}
		ctx := c.Request.Context()
		op, err := h.svc.AdminRestart(ctx, gameID)
		if err != nil {
			respondRuntimeError(c, h.logger, "admin runtimes restart", ctx, err)
			return
		}
		c.JSON(http.StatusAccepted, runtimeOperationToWire(op))
	}
}

// Patch handles POST /api/v1/admin/runtimes/{game_id}/patch.
func (h *AdminRuntimesHandlers) Patch() gin.HandlerFunc {
	if h == nil || h.svc == nil {
		return handlers.NotImplemented("adminRuntimesPatch")
	}
	return func(c *gin.Context) {
		var req runtimePatchRequestWire
		if err := c.ShouldBindJSON(&req); err != nil {
			httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON")
			return
		}
		gameID, ok := parseGameIDParam(c)
		if !ok {
			return
		}
		ctx := c.Request.Context()
		op, err := h.svc.AdminPatch(ctx, gameID, req.TargetVersion)
		if err != nil {
			respondRuntimeError(c, h.logger, "admin runtimes patch", ctx, err)
			return
		}
		c.JSON(http.StatusAccepted, runtimeOperationToWire(op))
	}
}

// ForceNextTurn handles POST /api/v1/admin/runtimes/{game_id}/force-next-turn.
func (h *AdminRuntimesHandlers) ForceNextTurn() gin.HandlerFunc {
	if h == nil || h.svc == nil {
		return handlers.NotImplemented("adminRuntimesForceNextTurn")
	}
	return func(c *gin.Context) {
		gameID, ok := parseGameIDParam(c)
		if !ok {
			return
		}
		ctx := c.Request.Context()
		op, err := h.svc.AdminForceNextTurn(ctx, gameID)
		if err != nil {
			respondRuntimeError(c, h.logger, "admin runtimes force-next-turn", ctx, err)
			return
		}
		c.JSON(http.StatusOK, runtimeOperationToWire(op))
	}
}

func respondRuntimeError(c *gin.Context, logger *zap.Logger, op string, ctx context.Context, err error) {
	switch {
	case errors.Is(err, runtime.ErrNotFound):
		httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "runtime record not found")
	case errors.Is(err, runtime.ErrInvalidInput),
		errors.Is(err, runtime.ErrPatchSemverIncompatible):
		httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, err.Error())
	case errors.Is(err, runtime.ErrConflict),
		errors.Is(err, runtime.ErrEngineVersionDisabled):
		httperr.Abort(c, http.StatusConflict, httperr.CodeConflict, err.Error())
	case errors.Is(err, runtime.ErrJobQueueFull):
		httperr.Abort(c, http.StatusServiceUnavailable, httperr.CodeServiceUnavailable, "runtime worker queue full, retry later")
	default:
		logger.Error(op+" failed",
			append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))...,
		)
		httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "internal error")
	}
}

// runtimeRecordWire mirrors `RuntimeRecord` from openapi.yaml. The
// schema declares `additionalProperties: true`, so we serialise the
// minimal documented shape.
type runtimeRecordWire struct {
	GameID string `json:"game_id"`
	Status string `json:"status"`
	CurrentContainerID string `json:"current_container_id,omitempty"`
	ImageRef string `json:"image_ref,omitempty"`
	StartedAt *string `json:"started_at,omitempty"`
	LastObservedAt *string `json:"last_observed_at,omitempty"`
}

func runtimeRecordToWire(r runtime.RuntimeRecord) runtimeRecordWire {
	out := runtimeRecordWire{
		GameID: r.GameID.String(),
		Status: r.Status,
		CurrentContainerID: r.CurrentContainerID,
		ImageRef: r.CurrentImageRef,
	}
	if r.StartedAt != nil {
		s := r.StartedAt.UTC().Format(timestampLayout)
		out.StartedAt = &s
	}
	if r.LastObservedAt != nil {
		s := r.LastObservedAt.UTC().Format(timestampLayout)
		out.LastObservedAt = &s
	}
	return out
}

// runtimeOperationWire mirrors `RuntimeOperation` from openapi.yaml.
type runtimeOperationWire struct {
	OperationID string `json:"operation_id"`
	GameID string `json:"game_id"`
	Op string `json:"op"`
	Status string `json:"status"`
	StartedAt string `json:"started_at"`
	FinishedAt *string `json:"finished_at,omitempty"`
	Error string `json:"error,omitempty"`
}

func runtimeOperationToWire(op runtime.OperationLog) runtimeOperationWire {
	out := runtimeOperationWire{
		OperationID: op.OperationID.String(),
		GameID: op.GameID.String(),
		Op: op.Op,
		Status: op.Status,
		StartedAt: op.StartedAt.UTC().Format(timestampLayout),
	}
	if op.FinishedAt != nil {
		s := op.FinishedAt.UTC().Format(timestampLayout)
		out.FinishedAt = &s
	}
	if op.ErrorMessage != "" {
		out.Error = op.ErrorMessage
	}
	return out
}

// runtimePatchRequestWire mirrors `RuntimePatchRequest`.
type runtimePatchRequestWire struct {
	TargetVersion string `json:"target_version"`
}
