// Package inventory implements stocks, stock movements (SM-...), manual
// stock-in/out, opname (OPN-...) and transfer (TRF-...). Rules follow
// business-rules.md §3; formats from §1; tables from database-analysis.md §3.
package inventory

import (
	"context"
	"encoding/json"
	"errors"
	"time"

	"github.com/shopspring/decimal"

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

// Movement mirrors stock_movements.
type Movement struct {
	ID             int64           `json:"id"`
	MovementNumber string          `json:"movement_number"`
	ProductID      int64           `json:"product_id"`
	WarehouseID    int64           `json:"warehouse_id"`
	MovementType   string          `json:"movement_type"`
	Quantity       decimal.Decimal `json:"quantity"`
	QuantityBefore decimal.Decimal `json:"quantity_before"`
	QuantityAfter  decimal.Decimal `json:"quantity_after"`
	ReferenceType  *string         `json:"reference_type"`
	ReferenceID    *int64          `json:"reference_id"`
	Notes          *string         `json:"notes"`
	CreatedBy      *int64          `json:"created_by"`
	DeletedAt      *time.Time      `json:"deleted_at"`
	CreatedAt      time.Time       `json:"created_at"`
	ProductName    string          `json:"product_name,omitempty"`
	ProductCode    string          `json:"product_code,omitempty"`
	WarehouseName  string          `json:"warehouse_name,omitempty"`
	Direction      string          `json:"direction,omitempty"`
}

// StockOutletLine is the per-warehouse qty for a StockLine.
type StockOutletLine struct {
	WarehouseID      int64           `json:"warehouse_id"`
	WarehouseName    string          `json:"warehouse_name"`
	Quantity         decimal.Decimal `json:"quantity"`
	ReservedQuantity decimal.Decimal `json:"reserved_quantity"`
}

// StockLine is one row of the stock list (SUM quantity per product).
//
// The three cost fields are POINTERS and are omitted for a caller that does
// not hold inventory.cost.view. Two reasons, both load-bearing:
//
//   - ONE SHAPE, TWO AUDIENCES. Availability and cost answer different
//     questions for different roles. A POS operator needs "is there stock";
//     a B2B buyer needs it to order; only an inventory or finance role needs
//     "what did we pay for it", and purchase_price/hpp/value ARE the margin.
//     Before this split one permission gate served both, so whoever passed the
//     gate received cost whether they needed it or not.
//   - ABSENT, NOT ZERO. A pointer nil serialises to nothing; a
//     decimal.Decimal zero would serialise to 0. Zero is a REAL cost (a free
//     sample, a promotional good), so a caller reading 0 cannot tell "you may
//     not see this" from "this was free", and a caller computing margin from
//     a silently-zeroed field gets a wrong number rather than an obvious
//     absence. Missing is unambiguous; 0 is not.
type StockLine struct {
	ProductID        int64           `json:"product_id"`
	ProductCode      string          `json:"product_code"`
	ProductName      string          `json:"product_name"`
	CategoryName     string          `json:"category_name"`
	UnitSymbol       string          `json:"unit_symbol"`
	Quantity         decimal.Decimal `json:"quantity"`
	MinStock         decimal.Decimal `json:"min_stock"`
	IsActive         bool            `json:"is_active"`
	ReservedQuantity decimal.Decimal `json:"reserved_quantity"`
	IsLowStock       bool            `json:"is_low_stock"`
	// Cost, present only for a caller holding inventory.cost.view.
	PurchasePrice *decimal.Decimal  `json:"purchase_price,omitempty"`
	HPP           *decimal.Decimal  `json:"hpp,omitempty"`
	Value         *decimal.Decimal  `json:"value,omitempty"`
	Outlets       []StockOutletLine `json:"outlets,omitempty"`
}

// StockSummaryValues is the raw aggregate from the repository: no permission
// decision has been applied, because the repository has no user on its context.
// Service.StockSummary turns it into the StockSummary a caller may see.
type StockSummaryValues struct {
	TotalProducts int
	TotalQty      decimal.Decimal
	TotalValue    decimal.Decimal
	LowStock      int
}

// StockSummary carries aggregate metrics over the entire matching stock set.
//
// TotalValue is a pointer for the same reason as StockLine's cost fields: it
// is the SUM of the cost figures, so serving it to a caller denied the
// per-row cost would leak the aggregate through the back door, and zeroing it
// would report a company-wide inventory valuation of Rp 0 as fact. Absent is
// unambiguous; zero is not. See StockLine.
type StockSummary struct {
	TotalProducts int              `json:"total_products"`
	TotalQty      decimal.Decimal  `json:"total_qty"`
	TotalValue    *decimal.Decimal `json:"total_value,omitempty"`
	LowStock      int              `json:"low_stock"`
}

// StockFilter carries pagination/search for ListStock.
type StockFilter struct {
	Page      int
	PerPage   int
	Search    string
	Warehouse int64 // 0 = all warehouses (aggregated)
	LowStock  bool
	SortBy    string
	SortDir   string
}

// MovementFilter carries pagination/search for ListMovements.
type MovementFilter struct {
	Page         int
	PerPage      int
	ProductID    int64
	WarehouseID  int64
	WarehouseIDs []int64
	Type         string
	DateFrom     string
	DateTo       string
	Search       string
}

type MovementSummary struct {
	TotalIn  decimal.Decimal `json:"total_in"`
	TotalOut decimal.Decimal `json:"total_out"`
	Total    int             `json:"total"`
}

type OpnameFilter struct {
	Page        int
	PerPage     int
	Status      string
	WarehouseID int64
}

type TransferFilter struct {
	Page    int
	PerPage int
	Status  string
	SortBy  string
	SortDir string
}

// Opname mirrors stock_opnames.
type Opname struct {
	ID           int64        `json:"id"`
	OpnameNumber string       `json:"opname_number"`
	WarehouseID  int64        `json:"warehouse_id"`
	OpnameDate   string       `json:"opname_date"`
	Status       string       `json:"status"`
	Notes        *string      `json:"notes"`
	CreatedBy    *int64       `json:"created_by"`
	ApprovedBy   *int64       `json:"approved_by"`
	DeletedAt    *time.Time   `json:"deleted_at"`
	Items        []OpnameItem `json:"items,omitempty"`
	// Joined on the opnames list only.
	WarehouseName string `json:"warehouse_name,omitempty"`
	ItemCount     int64  `json:"item_count,omitempty"`
}

// ErrStockFrozen is returned when a stock movement targets a warehouse whose
// opname is still open. The ERP's "Terbitkan & Kunci Stok" promises a frozen
// baseline; without this guard sales/purchases kept moving stock underneath a
// running count, so the system quantity the team was counting against was
// already stale when they submitted physical numbers.
var ErrStockFrozen = errors.New("stok gudang sedang dikunci oleh dokumen opname yang belum diselesaikan")

// OpnameItem mirrors stock_opname_items.
type OpnameItem struct {
	ID             int64            `json:"id"`
	StockOpnameID  int64            `json:"stock_opname_id"`
	ProductID      int64            `json:"product_id"`
	SystemQuantity decimal.Decimal  `json:"system_quantity"`
	PhysicalQty    *decimal.Decimal `json:"physical_quantity"`
	// Difference is nil until the line is counted. It used to be derived in
	// SQL as (COALESCE(physical,0) - system), which reported every uncounted
	// line as a full negative variance — a sheet nobody had touched read as
	// "everything is missing".
	Difference  *decimal.Decimal `json:"difference"`
	Notes       *string          `json:"notes"`
	ProductCode string           `json:"product_code,omitempty"`
	ProductName string           `json:"product_name,omitempty"`
	UnitSymbol  string           `json:"unit_symbol,omitempty"`
}

// OpnameInput mirrors the opname create request body.
type OpnameInput struct {
	WarehouseID int64             `json:"warehouse_id"`
	OpnameDate  string            `json:"opname_date"`
	Notes       *string           `json:"notes"`
	Items       []OpnameItemInput `json:"items"`
}

type OpnameItemInput struct {
	ProductID   int64            `json:"product_id"`
	PhysicalQty *decimal.Decimal `json:"physical_quantity"`
	Notes       *string          `json:"notes"`
}

// Transfer mirrors stock_transfers.
type Transfer struct {
	ID               int64           `json:"id"`
	TransferNumber   string          `json:"transfer_number"`
	FromWarehouseID  int64           `json:"from_warehouse_id"`
	ToWarehouseID    int64           `json:"to_warehouse_id"`
	TransferDate     string          `json:"transfer_date"`
	Status           string          `json:"status"`
	Notes            *string         `json:"notes"`
	CreatedBy        *int64          `json:"created_by"`
	ApprovedBy       *int64          `json:"approved_by"`
	VoucherNumber    *string         `json:"voucher_number"`
	DispatchedAt     *time.Time      `json:"dispatched_at"`
	VoucherSnapshot  json.RawMessage `json:"voucher_snapshot"`
	LifecycleVersion int             `json:"lifecycle_version"`
	Events           []TransferEvent `json:"events"`
	Items            []TransferItem  `json:"items"`
	DeletedAt        *time.Time      `json:"deleted_at"`
	CreatedAt        time.Time       `json:"created_at"`
	// Joined on the transfers list only.
	FromWarehouseName string `json:"from_warehouse_name,omitempty"`
	ToWarehouseName   string `json:"to_warehouse_name,omitempty"`
	ItemCount         int64  `json:"item_count,omitempty"`
}

// TransferItem mirrors stock_transfer_items.
type TransferItem struct {
	ID                int64           `json:"id"`
	StockTransferID   int64           `json:"stock_transfer_id"`
	ProductID         int64           `json:"product_id"`
	QuantityRequested decimal.Decimal `json:"quantity_requested"`
	QuantitySent      decimal.Decimal `json:"quantity_sent"`
	QuantityReceived  decimal.Decimal `json:"quantity_received"`
	QuantityTransit   decimal.Decimal `json:"quantity_transit"`
	QuantityDamaged   decimal.Decimal `json:"quantity_damaged"`
	QuantityLost      decimal.Decimal `json:"quantity_lost"`
	ProductCode       string          `json:"product_code,omitempty"`
	ProductName       string          `json:"product_name,omitempty"`
	UnitSymbol        string          `json:"unit_symbol,omitempty"`
}

// TransferInput mirrors the transfer create request body.
type TransferInput struct {
	FromWarehouseID int64               `json:"from_warehouse_id"`
	ToWarehouseID   int64               `json:"to_warehouse_id"`
	TransferDate    string              `json:"transfer_date"`
	Notes           *string             `json:"notes"`
	Items           []TransferItemInput `json:"items"`
}

type TransferItemInput struct {
	ProductID         int64           `json:"product_id"`
	QuantityRequested decimal.Decimal `json:"quantity_requested"`
}

type TransferEvent struct {
	ID         int64           `json:"id"`
	Action     string          `json:"action"`
	ActorID    int64           `json:"actor_id"`
	OccurredAt time.Time       `json:"occurred_at"`
	Reason     string          `json:"reason"`
	Payload    json.RawMessage `json:"payload"`
}

type TransferReceiptInput struct {
	RequestKey string                `json:"request_key"`
	Reason     string                `json:"reason"`
	Items      []TransferReceiptItem `json:"items"`
}

type TransferReceiptItem struct {
	ItemID  int64           `json:"item_id"`
	Good    decimal.Decimal `json:"good"`
	Damaged decimal.Decimal `json:"damaged"`
	Missing decimal.Decimal `json:"missing"`
}

// Tx is a physical transaction; pgx.Tx satisfies it. It doubles as the
// db.DBTX handed to tx-scoped store writes.
type Tx interface {
	db.DBTX
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

// Store is the inventory persistence seam (sqlc-backed).
type Store interface {
	// Begin starts a physical transaction (fakes: no-op).
	Begin(ctx context.Context) (Tx, error)

	// Stock row operations. q is a tx when the write must be atomic.
	StockRow(ctx context.Context, q db.DBTX, productID, warehouseID int64) (db.GetStockRowRow, error)
	LockStockRow(ctx context.Context, q db.DBTX, productID, warehouseID int64) (db.GetStockRowForUpdateRow, error)
	// WarehouseStockFrozen reports an open opname for the warehouse (freeze stock).
	WarehouseStockFrozen(ctx context.Context, q db.DBTX, warehouseID int64) (bool, error)
	EnsureStockRow(ctx context.Context, q db.DBTX, productID, warehouseID int64) error
	UpdateStock(ctx context.Context, q db.DBTX, p db.UpdateStockRowParams) error
	CreateMovement(ctx context.Context, q db.DBTX, p db.CreateMovementParams) (db.CreateMovementRow, error)

	// Queries (pool-scoped)
	ListStock(ctx context.Context, f StockFilter) ([]db.ListStockLinesRow, int, error)
	// StockSummary returns the UNAUTHENTICATED aggregate: the repository has no
	// request context and cannot know whether this caller may see cost, so it
	// must not decide. It returns the raw value; the service applies the
	// inventory.cost.view split (see Service.StockSummary). Returning the view
	// type here would put a permission decision in the persistence layer, which
	// is the seam that has no user on it.
	StockSummary(ctx context.Context, f StockFilter) (StockSummaryValues, error)
	ListMovements(ctx context.Context, f MovementFilter) ([]db.ListMovementsRow, int, error)
	ProductExists(ctx context.Context, id int64) (bool, error)
	WarehouseExists(ctx context.Context, id int64) (bool, error)
	// WarehousesForStores maps the caller's tenant store ids to the warehouse
	// ids that back them (migration 000045); used to scope reads/writes to a
	// kepala toko's own outlet.
	WarehousesForStores(ctx context.Context, storeIDs []int64) ([]int64, error)

	// Opname
	CreateOpname(ctx context.Context, p db.CreateOpnameParams) (int64, error)
	CreateOpnameOn(ctx context.Context, q db.DBTX, p db.CreateOpnameParams) (int64, error)
	OpnameByID(ctx context.Context, id int64) (db.GetOpnameByIDRow, error)
	LockOpnameByID(ctx context.Context, q db.DBTX, id int64) (db.LockOpnameByIDRow, error)
	ListOpnames(ctx context.Context, f OpnameFilter) ([]db.ListOpnamesRow, int, error)
	ListStockOn(ctx context.Context, q db.DBTX, f StockFilter) ([]db.ListStockLinesRow, int, error)
	CreateOpnameItem(ctx context.Context, p db.CreateOpnameItemParams) (int64, error)
	CreateOpnameItemOn(ctx context.Context, q db.DBTX, p db.CreateOpnameItemParams) (int64, error)
	OpnameItems(ctx context.Context, opnameID int64) ([]db.ListOpnameItemsRow, error)
	OpnameItemsOn(ctx context.Context, q db.DBTX, opnameID int64) ([]db.ListOpnameItemsRow, error)
	SetOpnameItemsPhysical(ctx context.Context, opnameID int64, items []OpnameItemInput) error
	UpdateOpnameStatus(ctx context.Context, id int64, status string) error
	UpdateOpnameStatusOn(ctx context.Context, q db.DBTX, id int64, status string) error
}
