package server

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

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

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

// timestampLayout matches the format used by other backend handlers
// (The implementation deviceSession serialisation). UTC, millisecond precision.
const timestampLayout = "2006-01-02T15:04:05.000Z07:00"

// respondAccountError maps user-package sentinels to the standard
// JSON error envelope. Unknown errors land on a 500.
func respondAccountError(c *gin.Context, logger *zap.Logger, op string, ctx context.Context, err error) {
	switch {
	case errors.Is(err, user.ErrAccountNotFound):
		httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "account not found")
	case errors.Is(err, user.ErrInvalidInput),
		errors.Is(err, user.ErrInvalidActor):
		httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, err.Error())
	case errors.Is(err, user.ErrInvalidTier):
		httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "tier is not supported")
	case errors.Is(err, user.ErrInvalidSanctionCode):
		httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "sanction_code is not supported")
	default:
		logger.Error(op+" failed",
			append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))...,
		)
		httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "service error")
	}
}

// accountResponseToWire renders the Account aggregate into the
// `AccountResponse` shape declared in openapi.yaml.
func accountResponseToWire(account user.Account) accountResponseWire {
	return accountResponseWire{Account: accountToWire(account)}
}

func accountToWire(account user.Account) accountWire {
	out := accountWire{
		UserID: account.UserID.String(),
		Email: account.Email,
		UserName: account.UserName,
		DisplayName: account.DisplayName,
		PreferredLanguage: account.PreferredLanguage,
		TimeZone: account.TimeZone,
		DeclaredCountry: account.DeclaredCountry,
		Entitlement: entitlementSnapshotToWire(account.Entitlement),
		ActiveSanctions: activeSanctionsToWire(account.ActiveSanctions),
		ActiveLimits: activeLimitsToWire(account.ActiveLimits),
		CreatedAt: account.CreatedAt.UTC().Format(timestampLayout),
		UpdatedAt: account.UpdatedAt.UTC().Format(timestampLayout),
	}
	return out
}

func entitlementSnapshotToWire(snap user.EntitlementSnapshot) entitlementSnapshotWire {
	out := entitlementSnapshotWire{
		PlanCode: snap.Tier,
		IsPaid: snap.IsPaid,
		Source: snap.Source,
		Actor: actorRefToWire(snap.Actor),
		ReasonCode: snap.ReasonCode,
		StartsAt: snap.StartsAt.UTC().Format(timestampLayout),
		MaxRegisteredRaceNames: snap.MaxRegisteredRaceNames,
		UpdatedAt: snap.UpdatedAt.UTC().Format(timestampLayout),
	}
	if snap.EndsAt != nil {
		formatted := snap.EndsAt.UTC().Format(timestampLayout)
		out.EndsAt = &formatted
	}
	return out
}

func activeSanctionsToWire(items []user.ActiveSanction) []activeSanctionWire {
	out := make([]activeSanctionWire, 0, len(items))
	for _, s := range items {
		entry := activeSanctionWire{
			SanctionCode: s.SanctionCode,
			Scope: s.Scope,
			ReasonCode: s.ReasonCode,
			Actor: actorRefToWire(s.Actor),
			AppliedAt: s.AppliedAt.UTC().Format(timestampLayout),
		}
		if s.ExpiresAt != nil {
			formatted := s.ExpiresAt.UTC().Format(timestampLayout)
			entry.ExpiresAt = &formatted
		}
		out = append(out, entry)
	}
	return out
}

func activeLimitsToWire(items []user.ActiveLimit) []activeLimitWire {
	out := make([]activeLimitWire, 0, len(items))
	for _, l := range items {
		entry := activeLimitWire{
			LimitCode: l.LimitCode,
			Value: l.Value,
			ReasonCode: l.ReasonCode,
			Actor: actorRefToWire(l.Actor),
			AppliedAt: l.AppliedAt.UTC().Format(timestampLayout),
		}
		if l.ExpiresAt != nil {
			formatted := l.ExpiresAt.UTC().Format(timestampLayout)
			entry.ExpiresAt = &formatted
		}
		out = append(out, entry)
	}
	return out
}

func actorRefToWire(actor user.ActorRef) actorRefWire {
	return actorRefWire{Type: actor.Type, ID: actor.ID}
}

// parseTimePtr converts a wire timestamp pointer into a time.Time
// pointer. A nil or empty input yields nil. Invalid timestamps return
// an error that handlers map to ErrInvalidInput.
func parseTimePtr(raw *string) (*time.Time, error) {
	if raw == nil {
		return nil, nil
	}
	if *raw == "" {
		return nil, nil
	}
	t, err := time.Parse(time.RFC3339Nano, *raw)
	if err != nil {
		return nil, err
	}
	return &t, nil
}

// accountResponseWire mirrors `AccountResponse` in openapi.yaml.
type accountResponseWire struct {
	Account accountWire `json:"account"`
}

// accountWire mirrors `Account` in openapi.yaml.
type accountWire struct {
	UserID string `json:"user_id"`
	Email string `json:"email"`
	UserName string `json:"user_name"`
	DisplayName string `json:"display_name,omitempty"`
	PreferredLanguage string `json:"preferred_language"`
	TimeZone string `json:"time_zone"`
	DeclaredCountry string `json:"declared_country,omitempty"`
	Entitlement entitlementSnapshotWire `json:"entitlement"`
	ActiveSanctions []activeSanctionWire `json:"active_sanctions"`
	ActiveLimits []activeLimitWire `json:"active_limits"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
}

type entitlementSnapshotWire struct {
	PlanCode string `json:"plan_code"`
	IsPaid bool `json:"is_paid"`
	Source string `json:"source"`
	Actor actorRefWire `json:"actor"`
	ReasonCode string `json:"reason_code"`
	StartsAt string `json:"starts_at"`
	EndsAt *string `json:"ends_at,omitempty"`
	MaxRegisteredRaceNames int32 `json:"max_registered_race_names"`
	UpdatedAt string `json:"updated_at"`
}

type activeSanctionWire struct {
	SanctionCode string `json:"sanction_code"`
	Scope string `json:"scope"`
	ReasonCode string `json:"reason_code"`
	Actor actorRefWire `json:"actor"`
	AppliedAt string `json:"applied_at"`
	ExpiresAt *string `json:"expires_at,omitempty"`
}

type activeLimitWire struct {
	LimitCode string `json:"limit_code"`
	Value int32 `json:"value"`
	ReasonCode string `json:"reason_code"`
	Actor actorRefWire `json:"actor"`
	AppliedAt string `json:"applied_at"`
	ExpiresAt *string `json:"expires_at,omitempty"`
}

type actorRefWire struct {
	Type string `json:"type"`
	ID string `json:"id,omitempty"`
}
