// Package catalog implements master data: categories, units, products.
// Rules follow business-rules.md §1 (codes) and docs/database-analysis.md
// table definitions. Product/unit/category deletion rules: category & unit
// deletion is RESTRICTed while products reference them.
package catalog

import (
	"context"
	"time"

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

// CategoryView is the API shape for a category (matches db.GetCategoryByIDRow).
type CategoryView struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	IsActive    bool   `json:"is_active"`
}

// UnitView is the API shape for a unit (matches db.GetUnitByIDRow).
type UnitView struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"`
	Symbol      string `json:"symbol"`
	Description string `json:"description"`
}

// ProductView is the API response shape for a product. Persistence uses the
// flattened db.ListProductsRow/GetProductByIDRow; this view re-nests the
// category/unit so the JSON contract keeps the Laravel shape.
type ProductView struct {
	ID          int64         `json:"id"`
	Code        string        `json:"code"`
	Name        string        `json:"name"`
	Description *string       `json:"description"`
	CategoryID  int64         `json:"category_id"`
	UnitID      int64         `json:"unit_id"`
	BrandID     *int64        `json:"brand_id"`
	BrandName   *string       `json:"brand_name"`
	Category    *CategoryView `json:"category,omitempty"`
	Unit        *UnitView     `json:"unit,omitempty"`
	// PurchasePrice (HPP) is the margin, so it is served only to callers
	// holding inventory.cost.view (the key migration 000103 grants to the four
	// valuation roles). Pointer + omitempty: a denied caller does not receive
	// the key at all, keeping "you may not see this" distinguishable from a
	// genuine zero cost — the contract inventory.StockLine.PurchasePrice sets.
	PurchasePrice *decimal.Decimal `json:"purchase_price,omitempty"`
	SellingPrice  decimal.Decimal  `json:"selling_price"`
	MinStock      decimal.Decimal  `json:"min_stock"`
	TotalStock    decimal.Decimal  `json:"total_stock"`
	IsActive      bool             `json:"is_active"`
	IsFeatured    bool             `json:"is_featured"`
	Image         *string          `json:"image"`
	ImageUrl      *string          `json:"image_url,omitempty"`
	DeletedAt     *time.Time       `json:"deleted_at"`
	CreatedAt     time.Time        `json:"created_at"`
	UpdatedAt     time.Time        `json:"updated_at"`
}

// BrandView is the API shape for a brand (business-rules.md §12).
type BrandView struct {
	ID          int64  `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	IsActive    bool   `json:"is_active"`
}

// ProductUnitView exposes one configured selling unit of a product. Quantities
// are expressed in the configured unit; ConversionFactor converts to base units.
type ProductUnitView struct {
	ID               int64            `json:"id"`
	ProductID        int64            `json:"product_id"`
	UnitID           int64            `json:"unit_id"`
	UnitName         string           `json:"unit_name"`
	UnitSymbol       string           `json:"unit_symbol"`
	ConversionFactor decimal.Decimal  `json:"conversion_factor"`
	SellingPrice     *decimal.Decimal `json:"selling_price"`
	MinPurchase      decimal.Decimal  `json:"min_purchase"`
	IsBase           bool             `json:"is_base"`
	IsActive         bool             `json:"is_active"`
}

// ProductBarcodeView exposes a scannable identifier, optionally bound to one
// configured product unit so a box scan and a piece scan resolve differently.
type ProductBarcodeView struct {
	ID            int64  `json:"id"`
	ProductID     int64  `json:"product_id"`
	ProductUnitID *int64 `json:"product_unit_id"`
	Barcode       string `json:"barcode"`
	IsPrimary     bool   `json:"is_primary"`
}

// BarcodeLookupView answers a POS/B2B scan: which product, which configured
// unit, and the configured price and minimum purchase for that unit.
type BarcodeLookupView struct {
	Barcode      string           `json:"barcode"`
	ProductID    int64            `json:"product_id"`
	ProductCode  string           `json:"product_code"`
	ProductName  string           `json:"product_name"`
	IsActive     bool             `json:"is_active"`
	SellingPrice decimal.Decimal  `json:"selling_price"`
	ProductUnit  *ProductUnitView `json:"product_unit"`
}

// CategoryInput mirrors the openapi request body. Description is a pointer so an
// omitted field on a partial UPDATE is preserved rather than wiped to "".
type CategoryInput struct {
	Name        string  `json:"name"`
	Description *string `json:"description"`
	IsActive    bool    `json:"is_active"`
}

type UnitInput struct {
	Name        string `json:"name"`
	Symbol      string `json:"symbol"`
	Description string `json:"description"`
}

type ProductInput struct {
	Name          string          `json:"name"`
	Description   *string         `json:"description"`
	CategoryID    int64           `json:"category_id"`
	UnitID        int64           `json:"unit_id"`
	BrandID       *int64          `json:"brand_id"`
	PurchasePrice decimal.Decimal `json:"purchase_price"`
	SellingPrice  decimal.Decimal `json:"selling_price"`
	MinStock      decimal.Decimal `json:"min_stock"`
	IsActive      bool            `json:"is_active"`
	// IsFeatured is a pointer so an omitted field on a partial UPDATE preserves
	// the stored flag; an explicit false clears it. Create defaults to false.
	IsFeatured *bool   `json:"is_featured"`
	Image      *string `json:"image"`
}

type BrandInput struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	IsActive    bool   `json:"is_active"`
}

type ProductUnitInput struct {
	UnitID           int64            `json:"unit_id"`
	ConversionFactor decimal.Decimal  `json:"conversion_factor"`
	SellingPrice     *decimal.Decimal `json:"selling_price"`
	MinPurchase      decimal.Decimal  `json:"min_purchase"`
	IsBase           bool             `json:"is_base"`
	IsActive         bool             `json:"is_active"`
}

type ProductBarcodeInput struct {
	ProductUnitID *int64 `json:"product_unit_id"`
	Barcode       string `json:"barcode"`
	IsPrimary     bool   `json:"is_primary"`
}

// Store is the catalog persistence seam (sqlc-backed in production, fake in tests).
type Store interface {
	CategoryByID(ctx context.Context, id int64) (db.GetCategoryByIDRow, error)
	UnitByID(ctx context.Context, id int64) (db.GetUnitByIDRow, error)
	ListCategories(ctx context.Context, f CatalogFilter) ([]db.ListCategoriesRow, int, error)
	ListUnits(ctx context.Context, f CatalogFilter) ([]db.ListUnitsRow, int, error)
	CreateCategory(ctx context.Context, p db.CreateCategoryParams) (int64, error)
	UpdateCategory(ctx context.Context, p db.UpdateCategoryParams) error
	DeleteCategory(ctx context.Context, id int64) error
	CategoryNameExists(ctx context.Context, name string, exceptID int64) (bool, error)
	CreateUnit(ctx context.Context, p db.CreateUnitParams) (int64, error)
	UpdateUnit(ctx context.Context, p db.UpdateUnitParams) error
	DeleteUnit(ctx context.Context, id int64) error
	UnitNameExists(ctx context.Context, name string, exceptID int64) (bool, error)
	// product queries (soft-delete aware: WHERE deleted_at IS NULL)
	ListProducts(ctx context.Context, filter ProductFilter) ([]db.ListProductsRow, int, error)
	ProductByID(ctx context.Context, id int64) (db.GetProductByIDRow, error)
	CreateProduct(ctx context.Context, p db.CreateProductParams) (db.CreateProductRow, error)
	UpdateProduct(ctx context.Context, p db.UpdateProductParams) error
	DeleteProduct(ctx context.Context, id int64) error // soft delete
	// brands
	BrandByID(ctx context.Context, id int64) (db.Brand, error)
	ListBrands(ctx context.Context, onlyActive bool) ([]db.Brand, error)
	CreateBrand(ctx context.Context, p db.CreateBrandParams) (db.Brand, error)
	UpdateBrand(ctx context.Context, p db.UpdateBrandParams) error
	DeleteBrand(ctx context.Context, id int64) error
	BrandNameExists(ctx context.Context, name string, exceptID int64) (bool, error)
	// configured product units
	ListProductUnits(ctx context.Context, productID int64, onlyActive bool) ([]db.ListProductUnitsRow, error)
	ProductUnitByID(ctx context.Context, id int64) (db.GetProductUnitByIDRow, error)
	CreateProductUnit(ctx context.Context, p db.CreateProductUnitParams) (int64, error)
	UpdateProductUnit(ctx context.Context, p db.UpdateProductUnitParams) error
	DeleteProductUnit(ctx context.Context, id int64) error
	// product barcodes
	ListProductBarcodes(ctx context.Context, productID int64) ([]db.ProductBarcode, error)
	ProductBarcodeByID(ctx context.Context, id int64) (db.ProductBarcode, error)
	CreateProductBarcode(ctx context.Context, p db.CreateProductBarcodeParams) (db.ProductBarcode, error)
	DeleteProductBarcode(ctx context.Context, id int64) error
	LookupProductByBarcode(ctx context.Context, barcode string) (db.LookupProductByBarcodeRow, error)
}

// CatalogFilter carries pagination/search for ListCategories and ListUnits,
// the same shape as partners.ListFilter for the order-book lists.
type CatalogFilter struct {
	Page       int
	PerPage    int
	Search     string
	OnlyActive bool
}

// ProductFilter carries pagination/search for ListProducts.
type ProductFilter struct {
	Page       int
	PerPage    int
	Search     string
	IsActive   *bool
	CategoryID *int64
	BrandID    *int64
	// InStock filters to products with available stock (> 0) server-side.
	InStock *bool
	// Sort: name | price_asc | price_desc | newest | popularity (empty/
	// relevance → default code order). Applied in SQL, not the client.
	Sort string
	// SortBy/SortDir are the ERP table's generic column sort
	// (?sort_by=selling_price&sort_dir=desc). They exist because the ERP
	// headers are per-column, not the storefront's fixed choices — the list
	// used to ignore them entirely, so every header click returned the same
	// page order.
	SortBy  string
	SortDir string
}
