package membership

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

	"github.com/jackc/pgx/v5/pgtype"
	"github.com/shopspring/decimal"

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

// Sentinel domain errors mapped to HTTP in the handler.
var (
	ErrNotFound                 = errors.New("not found")
	ErrCustomerInvalid          = errors.New("customer does not exist")
	ErrAlreadyMember            = errors.New("customer is already a member")
	ErrTierInvalid              = errors.New("tier must be bronze, gold or platinum")
	ErrJoinedDate               = errors.New("joined_date must be YYYY-MM-DD")
	ErrReasonRequired           = errors.New("reason is required when the tier changes")
	ErrTierNotFound             = errors.New("membership tier not found")
	ErrTierInactive             = errors.New("membership tier inactive")
	ErrTierCodeInvalid          = errors.New("tier code must be non-empty")
	ErrTierDiscountTypeInvalid  = errors.New("discount_type must be percentage or fixed")
	ErrTierDiscountValueInvalid = errors.New("discount_value must be >= 0")
)

const seqKeyMember = "mbr"

// Service implements the confirmed membership use cases. Benefit resolution is
// absent on purpose: the formulas are unresolved (business-rules.md §20).
type Service struct {
	store Store
	seq   Seq
	loc   *time.Location
}

func NewService(store Store, seq Seq, loc *time.Location) *Service {
	return &Service{store: store, seq: seq, loc: loc}
}

func (s *Service) List(ctx context.Context, f Filter) ([]View, int, error) {
	if f.Page < 1 {
		f.Page = 1
	}
	if f.PerPage < 1 || f.PerPage > 100 {
		f.PerPage = 20
	}
	if f.Tier != "" && !Tier(f.Tier).Valid() {
		return nil, 0, ErrTierInvalid
	}
	rows, total, err := s.store.List(ctx, f)
	if err != nil {
		return nil, 0, err
	}
	out := make([]View, 0, len(rows))
	for _, row := range rows {
		v := view(row)
		s.enrichView(ctx, &v)
		out = append(out, v)
	}
	return out, total, nil
}

func (s *Service) Get(ctx context.Context, id int64) (*View, error) {
	row, err := s.store.MembershipByID(ctx, nil, id)
	if err != nil {
		return nil, err
	}
	v := view(row)
	s.enrichView(ctx, &v)
	return &v, nil
}

// ByCustomer resolves the membership of a customer, or ErrNotFound when the
// customer is not a member. Callers use this to distinguish a member from an
// ordinary customer; both may transact (§13.1).
func (s *Service) ByCustomer(ctx context.Context, customerID int64) (*View, error) {
	row, err := s.store.MembershipByCustomer(ctx, nil, customerID)
	if err != nil {
		return nil, err
	}
	v := view(row)
	s.enrichView(ctx, &v)
	return &v, nil
}

// ByUser resolves the membership behind an authenticated user, walking the
// User -> Customer -> Membership chain.
func (s *Service) ByUser(ctx context.Context, userID int64) (*View, error) {
	row, err := s.store.MembershipByUser(ctx, nil, userID)
	if err != nil {
		return nil, err
	}
	v := view(row)
	s.enrichView(ctx, &v)
	return &v, nil
}

func (s *Service) ByNumber(ctx context.Context, number string) (*View, error) {
	number = strings.TrimSpace(number)
	if number == "" {
		return nil, ErrNotFound
	}
	row, err := s.store.MembershipByNumber(ctx, nil, number)
	if err != nil {
		return nil, err
	}
	v := view(row)
	s.enrichView(ctx, &v)
	return &v, nil
}

// Create registers an existing customer as a member and records the initial
// tier in the audit history within one transaction.
func (s *Service) Create(ctx context.Context, in CreateInput, actorID *int64) (*View, error) {
	if !in.Tier.Valid() {
		return nil, ErrTierInvalid
	}
	joined, err := parseDate(in.JoinedDate, s.loc)
	if err != nil {
		return nil, err
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	exists, err := s.store.CustomerExists(ctx, tx, in.CustomerID)
	if err != nil {
		return nil, err
	}
	if !exists {
		return nil, ErrCustomerInvalid
	}

	number, err := s.memberNumber(ctx, tx)
	if err != nil {
		return nil, err
	}
	id, err := s.store.Create(ctx, tx, db.CreateMembershipParams{
		MemberNumber: number,
		CustomerID:   in.CustomerID,
		Tier:         string(in.Tier),
		JoinedDate:   pgtype.Date{Time: joined, Valid: true},
		IsActive:     true,
		Notes:        textOrNil(in.Notes),
		CreatedBy:    int8OrNil(actorID),
	})
	if err != nil {
		return nil, err
	}
	if err := s.store.RecordTierChange(ctx, tx, db.CreateMembershipTierHistoryParams{
		MembershipID: id,
		ToTier:       string(in.Tier),
		Reason:       "membership created",
		ChangedBy:    int8OrNil(actorID),
	}); err != nil {
		return nil, err
	}
	row, err := s.store.MembershipByID(ctx, tx, id)
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	v := view(row)
	s.enrichView(ctx, &v)
	return &v, nil
}

// Update changes tier and/or active state. A tier change requires a reason and
// appends an audit entry; no automatic upgrade or downgrade is applied because
// the policy is unresolved.
func (s *Service) Update(ctx context.Context, id int64, in UpdateInput, actorID *int64) (*View, error) {
	if !in.Tier.Valid() {
		return nil, ErrTierInvalid
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	existing, err := s.store.MembershipByID(ctx, tx, id)
	if err != nil {
		return nil, err
	}

	tierChanged := existing.Tier != string(in.Tier)
	if tierChanged && strings.TrimSpace(in.Reason) == "" {
		return nil, ErrReasonRequired
	}

	isActive := existing.IsActive
	if in.IsActive != nil {
		isActive = *in.IsActive
	}
	notes := existing.Notes
	if in.Notes != nil {
		notes = pgtype.Text{String: *in.Notes, Valid: true}
	}

	if err := s.store.Update(ctx, tx, db.UpdateMembershipParams{
		ID:       id,
		Tier:     string(in.Tier),
		IsActive: isActive,
		Notes:    notes,
	}); err != nil {
		return nil, err
	}
	if tierChanged {
		if err := s.store.RecordTierChange(ctx, tx, db.CreateMembershipTierHistoryParams{
			MembershipID: id,
			FromTier:     pgtype.Text{String: existing.Tier, Valid: true},
			ToTier:       string(in.Tier),
			Reason:       strings.TrimSpace(in.Reason),
			ChangedBy:    int8OrNil(actorID),
		}); err != nil {
			return nil, err
		}
	}
	row, err := s.store.MembershipByID(ctx, tx, id)
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	v := view(row)
	s.enrichView(ctx, &v)
	return &v, nil
}

func (s *Service) TierHistory(ctx context.Context, id int64) ([]TierChangeView, error) {
	if _, err := s.store.MembershipByID(ctx, nil, id); err != nil {
		return nil, err
	}
	rows, err := s.store.TierHistory(ctx, id)
	if err != nil {
		return nil, err
	}
	out := make([]TierChangeView, 0, len(rows))
	for _, r := range rows {
		entry := TierChangeView{
			ID:        r.ID,
			ToTier:    Tier(r.ToTier),
			Reason:    r.Reason,
			ChangedBy: int8Ptr(r.ChangedBy),
			CreatedAt: r.CreatedAt.Time,
		}
		if r.FromTier.Valid {
			from := Tier(r.FromTier.String)
			entry.FromTier = &from
		}
		out = append(out, entry)
	}
	return out, nil
}

// TierConfigHistory returns the config audit trail for one tier: every create
// and every config change, newest first.
//
// This is the record of what a tier is WORTH. TierHistory above answers "which
// tier is this member on and when did it change", which is a different question
// and a different table — the two were previously confusable because only one
// of them was readable.
//
// The tier is fetched first so an unknown id is a 404 rather than an empty list:
// "this tier has never been configured" and "this tier does not exist" are
// different answers and only the second one is an error.
func (s *Service) TierConfigHistory(ctx context.Context, id int64) ([]TierConfigChangeView, error) {
	if _, err := s.store.GetTier(ctx, nil, id); err != nil {
		return nil, err
	}
	rows, err := s.store.TierConfigHistory(ctx, id)
	if err != nil {
		return nil, err
	}
	out := make([]TierConfigChangeView, 0, len(rows))
	for _, r := range rows {
		entry := TierConfigChangeView{
			ID:               r.ID,
			MembershipTierID: r.MembershipTierID,
			ToDiscountType:   r.ToDiscountType,
			ToDiscountValue:  r.ToDiscountValue,
			ChangedBy:        int8Ptr(r.ChangedBy),
			ChangedAt:        r.ChangedAt.Time,
		}
		if r.FromDiscountType.Valid {
			v := r.FromDiscountType.String
			entry.FromDiscountType = &v
		}
		if r.FromDiscountValue.Valid {
			v := r.FromDiscountValue.Decimal
			entry.FromDiscountValue = &v
		}
		if r.ToMinOrders.Valid {
			v := r.ToMinOrders.Int32
			entry.ToMinOrders = &v
		}
		if r.FromMinOrders.Valid {
			v := r.FromMinOrders.Int32
			entry.FromMinOrders = &v
		}
		if r.ToMinSpend.Valid {
			v := r.ToMinSpend.Decimal
			entry.ToMinSpend = &v
		}
		if r.FromMinSpend.Valid {
			v := r.FromMinSpend.Decimal
			entry.FromMinSpend = &v
		}
		if r.Reason.Valid {
			entry.Reason = r.Reason.String
		}
		out = append(out, entry)
	}
	return out, nil
}

// CreateTierInput carries the fields an admin may set when creating a tier
// configuration (STEP 0, business-rules.md §13.1).
type CreateTierInput struct {
	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"`
}

// UpdateTierInput carries the fields an admin may change. Nil fields are left
// unchanged (PATCH-style), so a config history entry records only the deltas.
type UpdateTierInput struct {
	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"`
}

// GetTier returns a single tier configuration by id.
func (s *Service) GetTier(ctx context.Context, id int64) (*TierConfig, error) {
	if id <= 0 {
		return nil, ErrTierNotFound
	}
	row, err := s.store.GetTier(ctx, nil, id)
	if err != nil {
		return nil, err
	}
	c := tierConfig(row)
	return &c, nil
}

// ListTiers returns every tier configuration (read-only, no transaction).
func (s *Service) ListTiers(ctx context.Context) ([]TierConfig, error) {
	rows, err := s.store.ListTiers(ctx, nil)
	if err != nil {
		return nil, err
	}
	out := make([]TierConfig, 0, len(rows))
	for _, r := range rows {
		out = append(out, tierConfig(r))
	}
	return out, nil
}

// CreateTier validates and persists a new tier configuration, recording its
// initial audit entry. Every config change is append-only (§13.1).
func (s *Service) CreateTier(ctx context.Context, in CreateTierInput, actorID *int64) (*TierConfig, error) {
	if strings.TrimSpace(in.Code) == "" {
		slug := strings.ToLower(strings.TrimSpace(in.Name))
		var b strings.Builder
		for _, r := range slug {
			if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
				b.WriteRune(r)
			} else {
				b.WriteRune('_')
			}
		}
		sName := strings.Trim(b.String(), "_")
		if sName == "" {
			sName = "tier"
		}
		in.Code = fmt.Sprintf("tier_%s_%d", sName, time.Now().UnixNano()%100000)
	}
	if in.DiscountType != "percentage" && in.DiscountType != "fixed" {
		return nil, ErrTierDiscountTypeInvalid
	}
	if in.DiscountValue.LessThan(decimal.Zero) {
		return nil, ErrTierDiscountValueInvalid
	}
	if in.MinOrders < 0 {
		return nil, errors.New("min_orders must be >= 0")
	}
	if in.MinSpend.LessThan(decimal.Zero) {
		return nil, errors.New("min_spend must be >= 0")
	}

	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	id, err := s.store.CreateTier(ctx, tx, db.CreateMembershipTierParams{
		Code:          in.Code,
		Name:          in.Name,
		DiscountType:  in.DiscountType,
		DiscountValue: in.DiscountValue,
		MinOrders:     in.MinOrders,
		MinSpend:      in.MinSpend,
		IsActive:      in.IsActive,
	})
	if err != nil {
		return nil, err
	}
	if err := s.store.RecordTierConfigHistory(ctx, tx, db.CreateMembershipTierConfigHistoryParams{
		MembershipTierID: id,
		ToDiscountType:   in.DiscountType,
		ToDiscountValue:  in.DiscountValue,
		ToMinOrders:      pgtype.Int4{Int32: in.MinOrders, Valid: true},
		ToMinSpend:       decimal.NullDecimal{Decimal: in.MinSpend, Valid: true},
		Reason:           pgtype.Text{String: "tier created", Valid: true},
		ChangedBy:        int8OrNil(actorID),
	}); err != nil {
		return nil, err
	}
	row, err := s.store.GetTierByCode(ctx, tx, in.Code)
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	c := tierConfig(row)
	return &c, nil
}

// UpdateTier changes the provided fields of a tier configuration within one
// transaction and appends a config-history entry with the before/after values.
func (s *Service) UpdateTier(ctx context.Context, id int64, in UpdateTierInput, actorID *int64) (*TierConfig, error) {
	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	current, err := s.store.GetTier(ctx, tx, id)
	if err != nil {
		return nil, err
	}

	name := current.Name
	discountType := current.DiscountType
	discountValue := current.DiscountValue
	minOrders := current.MinOrders
	minSpend := current.MinSpend
	isActive := current.IsActive
	if in.Name != nil {
		name = *in.Name
	}
	if in.DiscountType != nil {
		if *in.DiscountType != "percentage" && *in.DiscountType != "fixed" {
			return nil, ErrTierDiscountTypeInvalid
		}
		discountType = *in.DiscountType
	}
	if in.DiscountValue != nil {
		if in.DiscountValue.LessThan(decimal.Zero) {
			return nil, ErrTierDiscountValueInvalid
		}
		discountValue = *in.DiscountValue
	}
	if in.MinOrders != nil {
		if *in.MinOrders < 0 {
			return nil, errors.New("min_orders must be >= 0")
		}
		minOrders = *in.MinOrders
	}
	if in.MinSpend != nil {
		if in.MinSpend.LessThan(decimal.Zero) {
			return nil, errors.New("min_spend must be >= 0")
		}
		minSpend = *in.MinSpend
	}
	if in.IsActive != nil {
		isActive = *in.IsActive
	}

	if err := s.store.UpdateTier(ctx, tx, db.UpdateMembershipTierParams{
		ID:            id,
		Name:          name,
		DiscountType:  discountType,
		DiscountValue: discountValue,
		MinOrders:     minOrders,
		MinSpend:      minSpend,
		IsActive:      isActive,
	}); err != nil {
		return nil, err
	}
	if err := s.store.RecordTierConfigHistory(ctx, tx, db.CreateMembershipTierConfigHistoryParams{
		MembershipTierID:  current.ID,
		FromDiscountType:  pgtype.Text{String: current.DiscountType, Valid: true},
		FromDiscountValue: decimal.NullDecimal{Decimal: current.DiscountValue, Valid: true},
		ToDiscountType:    discountType,
		ToDiscountValue:   discountValue,
		FromMinOrders:     pgtype.Int4{Int32: current.MinOrders, Valid: true},
		ToMinOrders:       pgtype.Int4{Int32: minOrders, Valid: true},
		FromMinSpend:      decimal.NullDecimal{Decimal: current.MinSpend, Valid: true},
		ToMinSpend:        decimal.NullDecimal{Decimal: minSpend, Valid: true},
		Reason:            pgtype.Text{String: "tier config updated", Valid: true},
		ChangedBy:         int8OrNil(actorID),
	}); err != nil {
		return nil, err
	}
	row, err := s.store.GetTier(ctx, tx, id)
	if err != nil {
		return nil, err
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	c := tierConfig(row)
	return &c, nil
}

// AccruePoints awards points for spend and auto-promotes the tier when a
// threshold is crossed (§20 membership default). 1 point per Rp 10,000 spent;
// thresholds: bronze <1000, gold >=1000, platinum >=5000. Change the constants
// when the business confirms the real benefit policy.
func (s *Service) AccruePoints(ctx context.Context, id int64, amountSpent decimal.Decimal) (*View, error) {
	if amountSpent.LessThan(decimal.Zero) {
		return nil, errors.New("amount_spent must be >= 0")
	}
	tx, err := s.store.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	row, err := s.store.MembershipByID(ctx, tx, id)
	if err != nil {
		return nil, err
	}
	gained := amountSpent.Div(decimal.NewFromInt(10000)).Floor().IntPart()
	newPoints := row.Points + gained
	newTier := tierForPoints(newPoints)

	params := db.SetMembershipPointsParams{
		ID:     id,
		Points: newPoints,
		Tier:   string(newTier),
	}
	if err := s.store.SetMembershipPoints(ctx, tx, params); err != nil {
		return nil, err
	}
	if string(newTier) != row.Tier {
		if err := s.store.RecordTierChange(ctx, tx, db.CreateMembershipTierHistoryParams{
			MembershipID: id,
			FromTier:     textOrNil(&row.Tier),
			ToTier:       string(newTier),
			Reason:       "auto-promote by points",
			ChangedBy:    pgtype.Int8{},
		}); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	return s.Get(ctx, id)
}

// tierForPoints maps an accrued point total to its tier (§20 default).
func tierForPoints(points int64) Tier {
	switch {
	case points >= 5000:
		return TierPlatinum
	case points >= 1000:
		return TierGold
	default:
		return TierBronze
	}
}

func tierConfig(r db.MembershipTier) TierConfig {
	return TierConfig{
		ID:            r.ID,
		Code:          r.Code,
		Name:          r.Name,
		DiscountType:  r.DiscountType,
		DiscountValue: r.DiscountValue,
		MinOrders:     r.MinOrders,
		MinSpend:      r.MinSpend,
		IsActive:      r.IsActive,
		CreatedAt:     r.CreatedAt.Time,
		UpdatedAt:     r.UpdatedAt.Time,
	}
}

// memberNumber builds MBR-YYYYMM-000001 using the atomic monthly sequence (§17).
func (s *Service) memberNumber(ctx context.Context, tx any) (string, error) {
	now := time.Now().In(s.loc)
	scope := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, s.loc)
	n, err := s.seq.Sequence(ctx, seqKeyMember, scope, tx)
	if err != nil {
		return "", fmt.Errorf("member number sequence: %w", err)
	}
	return fmt.Sprintf("MBR-%s-%06d", now.Format("200601"), n), nil
}

func (s *Service) enrichView(ctx context.Context, v *View) {
	if v == nil {
		return
	}
	total, count, err := s.store.CustomerOrderStats(ctx, nil, v.CustomerID)
	if err == nil {
		v.TotalSpent = total
		v.CompletedOrdersCount = count
	}
}

func view(r db.GetMembershipByIDRow) View {
	return View{
		ID:           r.ID,
		MemberNumber: r.MemberNumber,
		CustomerID:   r.CustomerID,
		CustomerCode: r.CustomerCode,
		CustomerName: r.CustomerName,
		UserID:       int8Ptr(r.UserID),
		Tier:         Tier(r.Tier),
		Points:       r.Points,
		JoinedDate:   r.JoinedDate,
		IsActive:     r.IsActive,
		Notes:        textPtr(r.Notes),
		CreatedAt:    r.CreatedAt.Time,
		UpdatedAt:    r.UpdatedAt.Time,
	}
}

func parseDate(v string, loc *time.Location) (time.Time, error) {
	v = strings.TrimSpace(v)
	if v == "" {
		return time.Now().In(loc), nil
	}
	t, err := time.ParseInLocation("2006-01-02", v, loc)
	if err != nil {
		return time.Time{}, ErrJoinedDate
	}
	return t, nil
}

func textOrNil(v *string) pgtype.Text {
	if v == nil {
		return pgtype.Text{}
	}
	return pgtype.Text{String: *v, Valid: true}
}

func textPtr(v pgtype.Text) *string {
	if !v.Valid {
		return nil
	}
	s := v.String
	return &s
}

func int8OrNil(v *int64) pgtype.Int8 {
	if v == nil {
		return pgtype.Int8{}
	}
	return pgtype.Int8{Int64: *v, Valid: true}
}

func int8Ptr(v pgtype.Int8) *int64 {
	if !v.Valid {
		return nil
	}
	x := v.Int64
	return &x
}
