// Package cashier implements cashier shift and drawer operations
// (business-rules §11.5): open shift with opening cash, cash in/out, close
// with actual cash and computed variance. Expected cash = opening + cash_in -
// cash_out + cash-method POS sales linked to the shift.
//
// DEFAULT PENDING CONFIRMATION (§20 #5): shifts are not bound to a
// register/store; a cashier may hold only one open shift at a time.
package cashier

import (
	"context"
	"errors"
	"time"

	"github.com/shopspring/decimal"

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

var (
	ErrNotFound     = errors.New("not found")
	ErrShiftOpen    = errors.New("cashier already has an open shift")
	ErrShiftNotOpen = errors.New("shift is not open")
	ErrForbidden    = errors.New("cashier shift operation is not permitted")
	ErrCashMismatch = errors.New("physical cash differs from expected cash; authorized override and reason required")
)

// Shift mirrors cashier_shifts.
type Shift struct {
	ID          int64  `json:"id"`
	ShiftNumber string `json:"shift_number"`
	CashierID   int64  `json:"cashier_id"`
	// CashierName resolves via users join on the list read; empty when the
	// row predates the join or the user is gone (never a wrong name).
	CashierName string           `json:"cashier_name"`
	Status      string           `json:"status"`
	OpenedAt    time.Time        `json:"opened_at"`
	OpeningCash decimal.Decimal  `json:"opening_cash"`
	ClosedAt    *time.Time       `json:"closed_at"`
	ActualCash  *decimal.Decimal `json:"actual_cash"`
	Variance    *decimal.Decimal `json:"variance"`
	// Drawer movement totals (from cashier_drawer_entries). Zero on rows that
	// have no entries — populated by List via the batched SumDrawerByShifts.
	CashIn     decimal.Decimal `json:"cash_in"`
	CashOut    decimal.Decimal `json:"cash_out"`
	Notes      *string         `json:"notes"`
	RegisterID *int64          `json:"register_id"`
	StoreID    *int64          `json:"store_id"`
	CreatedAt  time.Time       `json:"created_at"`
	UpdatedAt  time.Time       `json:"updated_at"`
}

// DrawerEntry mirrors cashier_drawer_entries.
type DrawerEntry struct {
	ID        int64           `json:"id"`
	ShiftID   int64           `json:"shift_id"`
	Type      string          `json:"type"`
	Amount    decimal.Decimal `json:"amount"`
	Notes     *string         `json:"notes"`
	CreatedBy *int64          `json:"created_by"`
	CreatedAt time.Time       `json:"created_at"`
}

// Summary is a shift plus its drawer totals and expected cash.
type Summary struct {
	Shift
	CashIn           decimal.Decimal            `json:"cash_in"`
	CashOut          decimal.Decimal            `json:"cash_out"`
	CashSales        decimal.Decimal            `json:"cash_sales"`
	NonCashSales     decimal.Decimal            `json:"non_cash_sales"`
	TransactionCount int64                      `json:"transaction_count"`
	SalesCount       int64                      `json:"sales_count"`
	SalesTotal       decimal.Decimal            `json:"sales_total"`
	ExpectedCash     decimal.Decimal            `json:"expected_cash"`
	Difference       *decimal.Decimal           `json:"difference,omitempty"`
	PaymentBreakdown map[string]decimal.Decimal `json:"payment_breakdown,omitempty"`
	Entries          []DrawerEntry              `json:"entries"`
	ClosureAudit     []ClosureAudit             `json:"closure_audit"`
}

// OpenInput is the open-shift request body.
type OpenInput struct {
	CashierID   int64           `json:"cashier_id"`
	OpeningCash decimal.Decimal `json:"opening_cash"`
	RegisterID  *int64          `json:"register_id"`
	StoreID     *int64          `json:"store_id"`
	Notes       *string         `json:"notes"`
}

// DrawerInput is a cash in/out request body.
type DrawerInput struct {
	Amount    decimal.Decimal `json:"amount"`
	Notes     *string         `json:"notes"`
	CreatedBy *int64          `json:"created_by"`
}

// CloseInput is the close-shift request body.
type CloseInput struct {
	ActualCash     decimal.Decimal `json:"actual_cash"`
	Notes          *string         `json:"notes,omitempty"`
	Override       bool            `json:"override"`
	OverrideReason string          `json:"override_reason,omitempty"`
}

// Filter carries pagination for the shift list.
type Filter struct {
	Page      int
	PerPage   int
	CashierID int64
	Status    string
}

// Seq allocates atomic counters (pkg/seq) — injected so tests fake it.
type Seq interface {
	Sequence(ctx context.Context, prefix string, date time.Time, tx any) (int64, 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
}

type ClosureAudit struct {
	ExpectedCash decimal.Decimal `json:"expected_cash"`
	ActualCash   decimal.Decimal `json:"actual_cash"`
	Difference   decimal.Decimal `json:"difference"`
	ActorID      int64           `json:"actor_id"`
	Reason       string          `json:"reason"`
	Override     bool            `json:"override"`
	CreatedAt    time.Time       `json:"created_at"`
}

// Store is the cashier persistence seam (sqlc-backed).
type Store interface {
	Begin(ctx context.Context) (Tx, error)
	CreateShift(ctx context.Context, q db.DBTX, p db.CreateCashierShiftParams) (db.CashierShift, error)
	SetShiftBinding(ctx context.Context, q db.DBTX, p db.SetShiftBindingParams) (db.CashierShift, error)
	GetShiftByID(ctx context.Context, q db.DBTX, id int64) (db.CashierShift, error)
	GetShiftByIDForUpdate(ctx context.Context, q db.DBTX, id int64) (db.CashierShift, error)
	GetOpenShiftByCashier(ctx context.Context, q db.DBTX, cashierID int64) (db.CashierShift, error)
	CloseShift(ctx context.Context, q db.DBTX, id int64, actualCash, variance decimal.Decimal, notes *string) error
	GetOpenShiftByCashierForUpdate(ctx context.Context, q db.DBTX, cashierID int64) (db.CashierShift, error)
	ListShifts(ctx context.Context, q db.DBTX, f db.ListCashierShiftsParams) ([]db.ListCashierShiftsRow, error)
	CountShifts(ctx context.Context, q db.DBTX, f db.CountCashierShiftsParams) (int64, error)
	CreateDrawerEntry(ctx context.Context, q db.DBTX, p db.CreateDrawerEntryParams) (db.CashierDrawerEntry, error)
	ListDrawerEntries(ctx context.Context, q db.DBTX, shiftID int64) ([]db.CashierDrawerEntry, error)
	SumDrawerByShift(ctx context.Context, q db.DBTX, shiftID int64) ([]db.SumDrawerByShiftRow, error)
	// Batched per-shift drawer sums (cash_in/cash_out) so list endpoints can
	// populate the money columns without N+1 Summary calls.
	SumDrawerByShifts(ctx context.Context, q db.DBTX, shiftIDs []int64) ([]db.SumDrawerByShiftsRow, error)
	SumCashSalesByShift(ctx context.Context, q db.DBTX, shiftID int64) (decimal.Decimal, error)
	SumNonCashSalesByShift(ctx context.Context, q db.DBTX, shiftID int64) (decimal.Decimal, error)
	CountOrdersByShift(ctx context.Context, q db.DBTX, shiftID int64) (int64, error)
	SetSalesOrderShift(ctx context.Context, q db.DBTX, orderID, shiftID int64) error
	ListClosureAudit(context.Context, db.DBTX, int64) ([]ClosureAudit, error)
	RecordClosureAudit(context.Context, db.DBTX, int64, ClosureAudit) error
	IsStoreActive(ctx context.Context, q db.DBTX, storeID int64) (bool, error)
	IsUserInActiveStore(ctx context.Context, q db.DBTX, userID, storeID int64) (bool, error)
	ActiveStoresForUser(ctx context.Context, q db.DBTX, userID int64) ([]int64, error)
}
