package user

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

	"github.com/google/uuid"
)

// ActorRef identifies the principal that produced an audit-bearing
// mutation. The wire shape mirrors the OpenAPI ActorRef schema. Type is
// one of "user", "admin", "system" in MVP. ID carries a user UUID for
// Type=="user", an admin username for Type=="admin", and is empty for
// Type=="system".
type ActorRef struct {
	Type string
	ID   string
}

// Validate rejects empty actor types and enforces the per-type shape
// of ID: a user actor requires a UUID id, a system actor must have an
// empty id. Other types pass through with no further check.
func (a ActorRef) Validate() error {
	t := strings.TrimSpace(a.Type)
	if t == "" {
		return ErrInvalidActor
	}
	switch t {
	case "user":
		if strings.TrimSpace(a.ID) == "" {
			return fmt.Errorf("%w: user actor requires id", ErrInvalidActor)
		}
		if _, err := uuid.Parse(a.ID); err != nil {
			return fmt.Errorf("%w: user actor id must be a uuid: %v", ErrInvalidActor, err)
		}
	case "system":
		if strings.TrimSpace(a.ID) != "" {
			return fmt.Errorf("%w: system actor must have an empty id", ErrInvalidActor)
		}
	}
	return nil
}

// Account is the read-side aggregate served by GetAccount and the
// admin/internal account fetches. It mirrors the OpenAPI `Account`
// schema; handlers convert it to the JSON wire shape.
type Account struct {
	UserID uuid.UUID
	Email string
	UserName string
	DisplayName string
	PreferredLanguage string
	TimeZone string
	DeclaredCountry string
	PermanentBlock bool
	Entitlement EntitlementSnapshot
	ActiveSanctions []ActiveSanction
	ActiveLimits []ActiveLimit
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt *time.Time
}

// AccountPage is the paged listing returned by ListAccounts.
type AccountPage struct {
	Items []Account
	Page int
	PageSize int
	Total int
}

// UpdateProfileInput carries the mutable profile fields exposed by
// `PATCH /api/v1/user/account/profile`. The pointer fields keep the
// "unspecified" / "explicit empty" distinction so a request that omits
// `display_name` does not clear the stored value.
type UpdateProfileInput struct {
	DisplayName *string
}

// UpdateSettingsInput carries the mutable settings fields exposed by
// `PATCH /api/v1/user/account/settings`.
type UpdateSettingsInput struct {
	PreferredLanguage *string
	TimeZone *string
}

// GetAccount loads the account aggregate for userID. Returns
// ErrAccountNotFound when the row is missing or has been soft-deleted.
//
// The entitlement snapshot is read through the in-memory cache when
// available, falling back to Postgres when the cache is cold (Warm not
// yet completed for a freshly-restarted process). Sanctions and limits
// are always read from Postgres.
func (s *Service) GetAccount(ctx context.Context, userID uuid.UUID) (Account, error) {
	if userID == uuid.Nil {
		return Account{}, ErrAccountNotFound
	}
	row, err := s.deps.Store.LookupAccount(ctx, userID)
	if err != nil {
		if errors.Is(err, ErrAccountNotFound) {
			return Account{}, err
		}
		return Account{}, fmt.Errorf("user get account: %w", err)
	}

	snapshot, err := s.lookupSnapshot(ctx, userID)
	if err != nil {
		return Account{}, fmt.Errorf("user get account: snapshot: %w", err)
	}

	sanctions, err := s.deps.Store.ListActiveSanctions(ctx, userID)
	if err != nil {
		return Account{}, fmt.Errorf("user get account: sanctions: %w", err)
	}

	limits, err := s.deps.Store.ListActiveLimits(ctx, userID)
	if err != nil {
		return Account{}, fmt.Errorf("user get account: limits: %w", err)
	}

	return assembleAccount(row, snapshot, sanctions, limits), nil
}

// ListAccounts returns a paged listing of live accounts ordered by
// `created_at DESC, user_id DESC`. Soft-deleted rows are excluded.
func (s *Service) ListAccounts(ctx context.Context, page, pageSize int) (AccountPage, error) {
	page, pageSize = normalisePaging(page, pageSize)

	rows, total, err := s.deps.Store.ListAccountRows(ctx, page, pageSize)
	if err != nil {
		return AccountPage{}, fmt.Errorf("user list accounts: %w", err)
	}
	out := AccountPage{
		Items: make([]Account, 0, len(rows)),
		Page: page,
		PageSize: pageSize,
		Total: total,
	}
	for _, row := range rows {
		snapshot, err := s.lookupSnapshot(ctx, row.UserID)
		if err != nil {
			return AccountPage{}, fmt.Errorf("user list accounts: snapshot for %s: %w", row.UserID, err)
		}
		sanctions, err := s.deps.Store.ListActiveSanctions(ctx, row.UserID)
		if err != nil {
			return AccountPage{}, fmt.Errorf("user list accounts: sanctions for %s: %w", row.UserID, err)
		}
		limits, err := s.deps.Store.ListActiveLimits(ctx, row.UserID)
		if err != nil {
			return AccountPage{}, fmt.Errorf("user list accounts: limits for %s: %w", row.UserID, err)
		}
		out.Items = append(out.Items, assembleAccount(row, snapshot, sanctions, limits))
	}
	return out, nil
}

// ResolveByEmail returns the user_id of the live account whose email
// matches the supplied (lower-cased, trimmed) value. Returns
// ErrAccountNotFound when no live row exists; soft-deleted rows are
// excluded.
func (s *Service) ResolveByEmail(ctx context.Context, email string) (uuid.UUID, error) {
	normalised := strings.ToLower(strings.TrimSpace(email))
	if normalised == "" {
		return uuid.Nil, ErrInvalidInput
	}
	id, ok, err := s.deps.Store.LookupAccountIDByEmail(ctx, normalised)
	if err != nil {
		return uuid.Nil, fmt.Errorf("user resolve by email: %w", err)
	}
	if !ok {
		return uuid.Nil, ErrAccountNotFound
	}
	return id, nil
}

// UpdateProfile patches the caller's mutable profile fields and
// returns the refreshed account aggregate.
func (s *Service) UpdateProfile(ctx context.Context, userID uuid.UUID, input UpdateProfileInput) (Account, error) {
	if userID == uuid.Nil {
		return Account{}, ErrAccountNotFound
	}
	if input.DisplayName != nil {
		// PATCH semantics: omitted fields are not touched. An explicit
		// empty value is allowed and clears the stored display name —
		// matching the OpenAPI description of UpdateProfileRequest.
		if err := s.deps.Store.UpdateAccountDisplayName(ctx, userID, *input.DisplayName, s.deps.Now().UTC()); err != nil {
			if errors.Is(err, ErrAccountNotFound) {
				return Account{}, err
			}
			return Account{}, fmt.Errorf("user update profile: %w", err)
		}
	}
	return s.GetAccount(ctx, userID)
}

// UpdateSettings patches the caller's mutable settings fields and
// returns the refreshed account aggregate.
func (s *Service) UpdateSettings(ctx context.Context, userID uuid.UUID, input UpdateSettingsInput) (Account, error) {
	if userID == uuid.Nil {
		return Account{}, ErrAccountNotFound
	}
	patch := settingsPatch{}
	if input.PreferredLanguage != nil {
		trimmed := strings.TrimSpace(*input.PreferredLanguage)
		if trimmed == "" {
			return Account{}, fmt.Errorf("%w: preferred_language must be non-empty", ErrInvalidInput)
		}
		patch.PreferredLanguage = &trimmed
	}
	if input.TimeZone != nil {
		trimmed := strings.TrimSpace(*input.TimeZone)
		if trimmed == "" {
			return Account{}, fmt.Errorf("%w: time_zone must be non-empty", ErrInvalidInput)
		}
		if _, err := time.LoadLocation(trimmed); err != nil {
			return Account{}, fmt.Errorf("%w: time_zone must be a valid IANA zone", ErrInvalidInput)
		}
		patch.TimeZone = &trimmed
	}
	if patch.empty() {
		return s.GetAccount(ctx, userID)
	}
	if err := s.deps.Store.UpdateAccountSettings(ctx, userID, patch, s.deps.Now().UTC()); err != nil {
		if errors.Is(err, ErrAccountNotFound) {
			return Account{}, err
		}
		return Account{}, fmt.Errorf("user update settings: %w", err)
	}
	return s.GetAccount(ctx, userID)
}

// lookupSnapshot consults the cache first and falls back to a direct
// Postgres read when the cache is cold. The cache miss is silent: the
// `Ready()` flag governs the readiness probe, not the live path.
func (s *Service) lookupSnapshot(ctx context.Context, userID uuid.UUID) (EntitlementSnapshot, error) {
	if cached, ok := s.deps.Cache.Get(userID); ok {
		return cached, nil
	}
	snap, err := s.deps.Store.LookupEntitlementSnapshot(ctx, userID)
	if err != nil {
		return EntitlementSnapshot{}, err
	}
	s.deps.Cache.Add(snap)
	return snap, nil
}

// GetEntitlementSnapshot returns the latest entitlement snapshot for
// userID through the cache-first read path. Used by the lobby package
// to evaluate the per-user `max_registered_race_names`
// quota at race-name registration time.
func (s *Service) GetEntitlementSnapshot(ctx context.Context, userID uuid.UUID) (EntitlementSnapshot, error) {
	return s.lookupSnapshot(ctx, userID)
}

func assembleAccount(row AccountRow, snapshot EntitlementSnapshot, sanctions []ActiveSanction, limits []ActiveLimit) Account {
	return Account{
		UserID: row.UserID,
		Email: row.Email,
		UserName: row.UserName,
		DisplayName: row.DisplayName,
		PreferredLanguage: row.PreferredLanguage,
		TimeZone: row.TimeZone,
		DeclaredCountry: row.DeclaredCountry,
		PermanentBlock: row.PermanentBlock,
		Entitlement: snapshot,
		ActiveSanctions: sanctions,
		ActiveLimits: limits,
		CreatedAt: row.CreatedAt,
		UpdatedAt: row.UpdatedAt,
		DeletedAt: row.DeletedAt,
	}
}

func normalisePaging(page, pageSize int) (int, int) {
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 50
	}
	if pageSize > 200 {
		pageSize = 200
	}
	return page, pageSize
}
