package user

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
)

// Tier values mirror the closed set documented in
// `backend/README.md` and enforced by CHECK constraints
// on `entitlement_records` and `entitlement_snapshots`.
const (
	TierFree = "free"
	TierMonthly = "monthly"
	TierYearly = "yearly"
	TierPermanent = "permanent"
)

// EntitlementSnapshot is the read-side view of the user's current
// entitlement. It mirrors the OpenAPI `EntitlementSnapshot` schema.
type EntitlementSnapshot struct {
	UserID uuid.UUID
	Tier string
	IsPaid bool
	Source string
	Actor ActorRef
	ReasonCode string
	StartsAt time.Time
	EndsAt *time.Time
	MaxRegisteredRaceNames int32
	UpdatedAt time.Time
}

// ApplyEntitlementInput carries the admin-supplied parameters of
// `POST /api/v1/admin/users/{user_id}/entitlements`. StartsAt and
// EndsAt are optional: when omitted the tier policy supplies sensible
// defaults (StartsAt = now, EndsAt = now + tier window for monthly /
// yearly, otherwise NULL).
type ApplyEntitlementInput struct {
	UserID uuid.UUID
	Tier string
	Source string
	Actor ActorRef
	ReasonCode string
	StartsAt *time.Time
	EndsAt *time.Time
}

// tierBinding is the static policy table that maps tiers to their
// derived attributes. The implementation keeps the values inline; later stages
// can move the table to configuration if marketing requirements
// diverge.
type tierBinding struct {
	IsPaid bool
	MaxRegisteredRaceNames int32
	DefaultDuration time.Duration // 0 = no expiry
}

var tierPolicy = map[string]tierBinding{
	TierFree: {IsPaid: false, MaxRegisteredRaceNames: 1, DefaultDuration: 0},
	TierMonthly: {IsPaid: true, MaxRegisteredRaceNames: 5, DefaultDuration: 30 * 24 * time.Hour},
	TierYearly: {IsPaid: true, MaxRegisteredRaceNames: 5, DefaultDuration: 365 * 24 * time.Hour},
	TierPermanent: {IsPaid: true, MaxRegisteredRaceNames: 5, DefaultDuration: 0},
}

// defaultFreeSnapshot returns the entitlement snapshot installed for a
// brand-new account at registration time.
func defaultFreeSnapshot(userID uuid.UUID, now time.Time) EntitlementSnapshot {
	binding := tierPolicy[TierFree]
	return EntitlementSnapshot{
		UserID: userID,
		Tier: TierFree,
		IsPaid: binding.IsPaid,
		Source: "system",
		Actor: ActorRef{Type: "system"},
		ReasonCode: "",
		StartsAt: now,
		EndsAt: nil,
		MaxRegisteredRaceNames: binding.MaxRegisteredRaceNames,
		UpdatedAt: now,
	}
}

// ApplyEntitlement validates the supplied input, derives missing
// attributes from the tier policy, persists an immutable record and a
// fresh snapshot atomically, refreshes the cache, and returns the
// up-to-date account aggregate.
//
// Tier downgrades are accepted: lobby state in the consolidated implementation enforces the
// "already-registered race names are never revoked on downgrade" rule
// at the lobby surface; user.Service is unconcerned with that
// invariant on the entitlement write path.
func (s *Service) ApplyEntitlement(ctx context.Context, input ApplyEntitlementInput) (Account, error) {
	if input.UserID == uuid.Nil {
		return Account{}, ErrAccountNotFound
	}
	binding, ok := tierPolicy[strings.TrimSpace(input.Tier)]
	if !ok {
		return Account{}, fmt.Errorf("%w: %q", ErrInvalidTier, input.Tier)
	}
	if err := input.Actor.Validate(); err != nil {
		return Account{}, err
	}
	source := strings.TrimSpace(input.Source)
	if source == "" {
		return Account{}, fmt.Errorf("%w: source must be non-empty", ErrInvalidInput)
	}

	now := s.deps.Now().UTC()
	startsAt := now
	if input.StartsAt != nil {
		startsAt = input.StartsAt.UTC()
	}
	var endsAt *time.Time
	switch {
	case input.EndsAt != nil:
		t := input.EndsAt.UTC()
		endsAt = &t
	case binding.DefaultDuration > 0:
		t := startsAt.Add(binding.DefaultDuration)
		endsAt = &t
	}

	snapshot := EntitlementSnapshot{
		UserID: input.UserID,
		Tier: input.Tier,
		IsPaid: binding.IsPaid,
		Source: source,
		Actor: input.Actor,
		ReasonCode: input.ReasonCode,
		StartsAt: startsAt,
		EndsAt: endsAt,
		MaxRegisteredRaceNames: binding.MaxRegisteredRaceNames,
		UpdatedAt: now,
	}

	persisted, err := s.deps.Store.ApplyEntitlementTx(ctx, snapshot)
	if err != nil {
		if errors.Is(err, ErrAccountNotFound) {
			return Account{}, err
		}
		return Account{}, fmt.Errorf("user apply entitlement: %w", err)
	}
	s.deps.Cache.Add(persisted)
	return s.GetAccount(ctx, input.UserID)
}
