package server

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

	"galaxy/backend/internal/lobby"
	"galaxy/backend/internal/server/httperr"
	"galaxy/backend/internal/telemetry"

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

// respondLobbyError maps lobby-package sentinel errors to the standard
// JSON error envelope. Unknown errors land on a 500.
func respondLobbyError(c *gin.Context, logger *zap.Logger, op string, ctx context.Context, err error) {
	switch {
	case errors.Is(err, lobby.ErrInvalidInput):
		httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, err.Error())
	case errors.Is(err, lobby.ErrNotFound):
		httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "resource was not found")
	case errors.Is(err, lobby.ErrForbidden):
		httperr.Abort(c, http.StatusForbidden, httperr.CodeForbidden, err.Error())
	case errors.Is(err, lobby.ErrConflict),
		errors.Is(err, lobby.ErrInvalidStatus),
		errors.Is(err, lobby.ErrRaceNameTaken),
		errors.Is(err, lobby.ErrEntitlementExceeded),
		errors.Is(err, lobby.ErrPendingExpired):
		httperr.Abort(c, http.StatusConflict, httperr.CodeConflict, err.Error())
	default:
		logger.Error(op+" failed",
			append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))...,
		)
		httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "service error")
	}
}

// parseGameIDParam reads `game_id` from the path. Writes 400 envelope on
// invalid input and returns false in that case.
func parseGameIDParam(c *gin.Context) (uuid.UUID, bool) {
	parsed, err := uuid.Parse(c.Param("game_id"))
	if err != nil {
		httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "game_id must be a valid UUID")
		return uuid.Nil, false
	}
	return parsed, true
}

func parseApplicationIDParam(c *gin.Context) (uuid.UUID, bool) {
	parsed, err := uuid.Parse(c.Param("application_id"))
	if err != nil {
		httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "application_id must be a valid UUID")
		return uuid.Nil, false
	}
	return parsed, true
}

func parseInviteIDParam(c *gin.Context) (uuid.UUID, bool) {
	parsed, err := uuid.Parse(c.Param("invite_id"))
	if err != nil {
		httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "invite_id must be a valid UUID")
		return uuid.Nil, false
	}
	return parsed, true
}

func parseMembershipIDParam(c *gin.Context) (uuid.UUID, bool) {
	parsed, err := uuid.Parse(c.Param("membership_id"))
	if err != nil {
		httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "membership_id must be a valid UUID")
		return uuid.Nil, false
	}
	return parsed, true
}

// gameSummaryWire mirrors `GameSummary` from openapi.yaml.
type gameSummaryWire struct {
	GameID           string  `json:"game_id"`
	GameName         string  `json:"game_name"`
	GameType         string  `json:"game_type"`
	Status           string  `json:"status"`
	OwnerUserID      *string `json:"owner_user_id,omitempty"`
	MinPlayers       int32   `json:"min_players"`
	MaxPlayers       int32   `json:"max_players"`
	EnrollmentEndsAt string  `json:"enrollment_ends_at"`
	CreatedAt        string  `json:"created_at"`
	UpdatedAt        string  `json:"updated_at"`
	CurrentTurn      int32   `json:"current_turn"`
}

// lobbyGameDetailWire mirrors `LobbyGameDetail` from openapi.yaml.
// `current_turn` is inherited from `gameSummaryWire`; the runtime
// fields below carry the runtime projection on top of it.
type lobbyGameDetailWire struct {
	gameSummaryWire
	Visibility          string  `json:"visibility"`
	Description         string  `json:"description,omitempty"`
	TurnSchedule        string  `json:"turn_schedule"`
	TargetEngineVersion string  `json:"target_engine_version"`
	StartGapHours       int32   `json:"start_gap_hours"`
	StartGapPlayers     int32   `json:"start_gap_players"`
	RuntimeStatus       string  `json:"runtime_status"`
	EngineHealth        string  `json:"engine_health,omitempty"`
	StartedAt           *string `json:"started_at,omitempty"`
	FinishedAt          *string `json:"finished_at,omitempty"`
}

func gameSummaryToWire(g lobby.GameRecord) gameSummaryWire {
	out := gameSummaryWire{
		GameID:           g.GameID.String(),
		GameName:         g.GameName,
		GameType:         g.Visibility,
		Status:           g.Status,
		MinPlayers:       g.MinPlayers,
		MaxPlayers:       g.MaxPlayers,
		EnrollmentEndsAt: g.EnrollmentEndsAt.UTC().Format(timestampLayout),
		CreatedAt:        g.CreatedAt.UTC().Format(timestampLayout),
		UpdatedAt:        g.UpdatedAt.UTC().Format(timestampLayout),
		CurrentTurn:      g.RuntimeSnapshot.CurrentTurn,
	}
	if g.OwnerUserID != nil {
		s := g.OwnerUserID.String()
		out.OwnerUserID = &s
	}
	return out
}

func lobbyGameDetailToWire(g lobby.GameRecord) lobbyGameDetailWire {
	out := lobbyGameDetailWire{
		gameSummaryWire:     gameSummaryToWire(g),
		Visibility:          g.Visibility,
		Description:         g.Description,
		TurnSchedule:        g.TurnSchedule,
		TargetEngineVersion: g.TargetEngineVersion,
		StartGapHours:       g.StartGapHours,
		StartGapPlayers:     g.StartGapPlayers,
		RuntimeStatus:       g.RuntimeSnapshot.RuntimeStatus,
		EngineHealth:        g.RuntimeSnapshot.EngineHealth,
	}
	if g.StartedAt != nil {
		s := g.StartedAt.UTC().Format(timestampLayout)
		out.StartedAt = &s
	}
	if g.FinishedAt != nil {
		s := g.FinishedAt.UTC().Format(timestampLayout)
		out.FinishedAt = &s
	}
	return out
}

// lobbyGameStateChangeWire mirrors `LobbyGameStateChange`.
type lobbyGameStateChangeWire struct {
	GameID        string `json:"game_id"`
	Status        string `json:"status"`
	RuntimeStatus string `json:"runtime_status,omitempty"`
}

func lobbyGameStateChangeToWire(g lobby.GameRecord) lobbyGameStateChangeWire {
	return lobbyGameStateChangeWire{
		GameID:        g.GameID.String(),
		Status:        g.Status,
		RuntimeStatus: g.RuntimeSnapshot.RuntimeStatus,
	}
}

// lobbyApplicationDetailWire mirrors `LobbyApplicationDetail`.
type lobbyApplicationDetailWire struct {
	ApplicationID   string  `json:"application_id"`
	GameID          string  `json:"game_id"`
	ApplicantUserID string  `json:"applicant_user_id"`
	RaceName        string  `json:"race_name"`
	Status          string  `json:"status"`
	CreatedAt       string  `json:"created_at"`
	DecidedAt       *string `json:"decided_at,omitempty"`
}

func lobbyApplicationDetailToWire(a lobby.Application) lobbyApplicationDetailWire {
	out := lobbyApplicationDetailWire{
		ApplicationID:   a.ApplicationID.String(),
		GameID:          a.GameID.String(),
		ApplicantUserID: a.ApplicantUserID.String(),
		RaceName:        a.RaceName,
		Status:          a.Status,
		CreatedAt:       a.CreatedAt.UTC().Format(timestampLayout),
	}
	if a.DecidedAt != nil {
		s := a.DecidedAt.UTC().Format(timestampLayout)
		out.DecidedAt = &s
	}
	return out
}

// lobbyInviteDetailWire mirrors `LobbyInviteDetail`.
type lobbyInviteDetailWire struct {
	InviteID      string  `json:"invite_id"`
	GameID        string  `json:"game_id"`
	InviterUserID string  `json:"inviter_user_id"`
	InvitedUserID *string `json:"invited_user_id,omitempty"`
	Code          *string `json:"code,omitempty"`
	RaceName      string  `json:"race_name"`
	Status        string  `json:"status"`
	CreatedAt     string  `json:"created_at"`
	ExpiresAt     string  `json:"expires_at"`
	DecidedAt     *string `json:"decided_at,omitempty"`
}

func lobbyInviteDetailToWire(i lobby.Invite) lobbyInviteDetailWire {
	out := lobbyInviteDetailWire{
		InviteID:      i.InviteID.String(),
		GameID:        i.GameID.String(),
		InviterUserID: i.InviterUserID.String(),
		RaceName:      i.RaceName,
		Status:        i.Status,
		CreatedAt:     i.CreatedAt.UTC().Format(timestampLayout),
		ExpiresAt:     i.ExpiresAt.UTC().Format(timestampLayout),
	}
	if i.InvitedUserID != nil {
		s := i.InvitedUserID.String()
		out.InvitedUserID = &s
	}
	if i.Code != "" {
		c := i.Code
		out.Code = &c
	}
	if i.DecidedAt != nil {
		s := i.DecidedAt.UTC().Format(timestampLayout)
		out.DecidedAt = &s
	}
	return out
}

// lobbyMembershipDetailWire mirrors `LobbyMembershipDetail`.
type lobbyMembershipDetailWire struct {
	MembershipID string  `json:"membership_id"`
	GameID       string  `json:"game_id"`
	UserID       string  `json:"user_id"`
	RaceName     string  `json:"race_name"`
	CanonicalKey string  `json:"canonical_key"`
	Status       string  `json:"status"`
	JoinedAt     string  `json:"joined_at"`
	RemovedAt    *string `json:"removed_at,omitempty"`
}

func lobbyMembershipDetailToWire(m lobby.Membership) lobbyMembershipDetailWire {
	out := lobbyMembershipDetailWire{
		MembershipID: m.MembershipID.String(),
		GameID:       m.GameID.String(),
		UserID:       m.UserID.String(),
		RaceName:     m.RaceName,
		CanonicalKey: m.CanonicalKey,
		Status:       m.Status,
		JoinedAt:     m.JoinedAt.UTC().Format(timestampLayout),
	}
	if m.RemovedAt != nil {
		s := m.RemovedAt.UTC().Format(timestampLayout)
		out.RemovedAt = &s
	}
	return out
}

// raceNameDetailWire mirrors `RaceNameDetail`.
type raceNameDetailWire struct {
	Name         string  `json:"name"`
	Canonical    string  `json:"canonical"`
	Status       string  `json:"status"`
	OwnerUserID  string  `json:"owner_user_id"`
	GameID       *string `json:"game_id,omitempty"`
	SourceGameID *string `json:"source_game_id,omitempty"`
	ReservedAt   *string `json:"reserved_at,omitempty"`
	ExpiresAt    *string `json:"expires_at,omitempty"`
	RegisteredAt *string `json:"registered_at,omitempty"`
}

func raceNameDetailToWire(e lobby.RaceNameEntry) raceNameDetailWire {
	out := raceNameDetailWire{
		Name:        e.Name,
		Canonical:   string(e.Canonical),
		Status:      e.Status,
		OwnerUserID: e.OwnerUserID.String(),
	}
	if e.GameID != (uuid.UUID{}) {
		s := e.GameID.String()
		out.GameID = &s
	}
	if e.SourceGameID != nil {
		s := e.SourceGameID.String()
		out.SourceGameID = &s
	}
	if e.ReservedAt != nil {
		s := e.ReservedAt.UTC().Format(timestampLayout)
		out.ReservedAt = &s
	}
	if e.ExpiresAt != nil {
		s := e.ExpiresAt.UTC().Format(timestampLayout)
		out.ExpiresAt = &s
	}
	if e.RegisteredAt != nil {
		s := e.RegisteredAt.UTC().Format(timestampLayout)
		out.RegisteredAt = &s
	}
	return out
}

// parseTimePtrField parses a wire timestamp pointer into a time.Time
// pointer. Empty / nil input yields nil. Invalid timestamps return an
// error.
func parseTimePtrField(raw *string, field string) (*time.Time, error) {
	if raw == nil || *raw == "" {
		return nil, nil
	}
	t, err := time.Parse(time.RFC3339Nano, *raw)
	if err != nil {
		return nil, errors.New(field + " must be RFC 3339")
	}
	return &t, nil
}
