// Package membership implements the confirmed part of customer membership
// (business-rules.md §13.1): the User -> Customer -> Membership identity split
// and exactly three tiers.
//
// Deliberately absent: benefit formulas, special prices, discounts, points, and
// automatic upgrade/downgrade/expiry. Those are UNRESOLVED (§20 items 1 and 4)
// and must not be invented. Every tier change is recorded in an append-only
// history so a confirmed policy can be reconstructed and audited later.
package membership

import (
	"context"
	"time"

	"github.com/shopspring/decimal"

	"github.com/TaruLab/keluargamart-backend/internal/db"
)

// Tier is a confirmed membership tier. Silver is not a member of this set and
// must not be added without new business confirmation.
type Tier string

const (
	TierBronze   Tier = "bronze"
	TierGold     Tier = "gold"
	TierPlatinum Tier = "platinum"
)

// Tiers lists the confirmed tiers in ascending order.
func Tiers() []Tier { return []Tier{TierBronze, TierGold, TierPlatinum} }

// Valid reports whether t is one of the confirmed tiers.
func (t Tier) Valid() bool {
	switch t {
	case TierBronze, TierGold, TierPlatinum:
		return true
	default:
		return false
	}
}

// View is the API shape for a membership. Benefit fields are intentionally
// missing until the formulas are confirmed.
type View struct {
	ID           int64     `json:"id"`
	MemberNumber string    `json:"member_number"`
	CustomerID   int64     `json:"customer_id"`
	CustomerCode string    `json:"customer_code"`
	CustomerName string    `json:"customer_name"`
	UserID       *int64    `json:"user_id"`
	Tier                 Tier            `json:"tier"`
	Points               int64           `json:"points"`
	TotalSpent           decimal.Decimal `json:"total_spent"`
	CompletedOrdersCount int64           `json:"completed_orders_count"`
	JoinedDate           string          `json:"joined_date"`
	IsActive     bool      `json:"is_active"`
	Notes        *string   `json:"notes"`
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
}

// TierChangeView is one append-only tier audit entry.
type TierChangeView struct {
	ID        int64     `json:"id"`
	FromTier  *Tier     `json:"from_tier"`
	ToTier    Tier      `json:"to_tier"`
	Reason    string    `json:"reason"`
	ChangedBy *int64    `json:"changed_by"`
	CreatedAt time.Time `json:"created_at"`
}

// TierConfigChangeView is one entry in the tier CONFIG audit trail — what a
// tier is worth, and who changed it. Distinct from TierChangeView, which is the
// per-member tier assignment trail.
//
// Every field is nullable on purpose. A "tier created" row has no FROM values
// because there was nothing before it, and rows written before migration 000077
// have no min_orders / min_spend values at all because those columns did not
// exist yet. Both are real history, not missing data, so they are exposed as
// null rather than back-filled with zeroes that would read as "the threshold
// used to be 0".
type TierConfigChangeView struct {
	ID                int64            `json:"id"`
	MembershipTierID  int64            `json:"membership_tier_id"`
	FromDiscountType  *string          `json:"from_discount_type"`
	FromDiscountValue *decimal.Decimal `json:"from_discount_value"`
	ToDiscountType    string           `json:"to_discount_type"`
	ToDiscountValue   decimal.Decimal  `json:"to_discount_value"`
	FromMinOrders     *int32           `json:"from_min_orders"`
	ToMinOrders       *int32           `json:"to_min_orders"`
	FromMinSpend      *decimal.Decimal `json:"from_min_spend"`
	ToMinSpend        *decimal.Decimal `json:"to_min_spend"`
	Reason            string           `json:"reason"`
	ChangedBy         *int64           `json:"changed_by"`
	ChangedAt         time.Time        `json:"changed_at"`
}

// TierConfig is the database-backed membership tier configuration (STEP 0,
// business-rules.md §13.1). The discount percentage/value is read from here at
// pricing time; the in-code fallback map is only used when no resolver is wired.
type TierConfig struct {
	ID            int64           `json:"id"`
	Code          string          `json:"code"`
	Name          string          `json:"name"`
	DiscountType  string          `json:"discount_type"`
	DiscountValue decimal.Decimal `json:"discount_value"`
	MinOrders     int32           `json:"min_orders"`
	MinSpend      decimal.Decimal `json:"min_spend"`
	IsActive      bool            `json:"is_active"`
	CreatedAt     time.Time       `json:"created_at"`
	UpdatedAt     time.Time       `json:"updated_at"`
}

// CreateInput registers an existing customer as a member.
type CreateInput struct {
	CustomerID int64   `json:"customer_id"`
	Tier       Tier    `json:"tier"`
	JoinedDate string  `json:"joined_date"`
	Notes      *string `json:"notes"`
}

// UpdateInput changes tier and/or active state. Reason is required when the tier
// changes so the audit trail explains every movement.
type UpdateInput struct {
	Tier     Tier    `json:"tier"`
	IsActive *bool   `json:"is_active"`
	Notes    *string `json:"notes"`
	Reason   string  `json:"reason"`
}

// Filter carries pagination and search for List.
type Filter struct {
	Page     int
	PerPage  int
	Search   string
	Tier     string
	IsActive *bool
}

// Store is the membership persistence seam (sqlc-backed in production).
type Store interface {
	Begin(ctx context.Context) (Tx, error)
	MembershipByID(ctx context.Context, q db.DBTX, id int64) (db.GetMembershipByIDRow, error)
	MembershipByCustomer(ctx context.Context, q db.DBTX, customerID int64) (db.GetMembershipByIDRow, error)
	MembershipByUser(ctx context.Context, q db.DBTX, userID int64) (db.GetMembershipByIDRow, error)
	MembershipByNumber(ctx context.Context, q db.DBTX, number string) (db.GetMembershipByIDRow, error)
	List(ctx context.Context, f Filter) ([]db.GetMembershipByIDRow, int, error)
	CustomerExists(ctx context.Context, q db.DBTX, id int64) (bool, error)
	CustomerOrderStats(ctx context.Context, q db.DBTX, customerID int64) (decimal.Decimal, int64, error)
	Create(ctx context.Context, q db.DBTX, p db.CreateMembershipParams) (int64, error)
	Update(ctx context.Context, q db.DBTX, p db.UpdateMembershipParams) error
	SetMembershipPoints(ctx context.Context, q db.DBTX, p db.SetMembershipPointsParams) error
	RecordTierChange(ctx context.Context, q db.DBTX, p db.CreateMembershipTierHistoryParams) error
	TierHistory(ctx context.Context, membershipID int64) ([]db.MembershipTierHistory, error)
	ListTiers(ctx context.Context, q db.DBTX) ([]db.MembershipTier, error)
	GetTier(ctx context.Context, q db.DBTX, id int64) (db.MembershipTier, error)
	GetTierByCode(ctx context.Context, q db.DBTX, code string) (db.MembershipTier, error)
	CreateTier(ctx context.Context, q db.DBTX, p db.CreateMembershipTierParams) (int64, error)
	UpdateTier(ctx context.Context, q db.DBTX, p db.UpdateMembershipTierParams) error
	RecordTierConfigHistory(ctx context.Context, q db.DBTX, p db.CreateMembershipTierConfigHistoryParams) error
	TierConfigHistory(ctx context.Context, tierID int64) ([]db.MembershipTierConfigHistory, error)
}

// Tx is a physical transaction; pgx.Tx satisfies it.
type Tx interface {
	db.DBTX
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

// Seq allocates atomic counters (pkg/seq) so member numbers never collide.
type Seq interface {
	Sequence(ctx context.Context, prefix string, date time.Time, tx any) (int64, error)
}
