package models

// Wire format for the Nearle POS terminal.
//
// These types mirror what the till actually publishes, field for field. The
// terminal is the fixed side of this contract: it is installed on a hundred
// machines that cannot all be updated at once, so the names here follow its
// JSON rather than this codebase's usual Go casing.
//
// The authoritative description lives in the terminal repository at
// docs/sync-contract.md.

// PosOrderItem is one line of a counter bill.
//
// Productid arrives as a string because the till stores catalogue ids as text.
// It carries the numeric products.productid this backend issued during a
// catalogue pull, so it parses back to an int on arrival.
type PosOrderItem struct {
	Productid string  `json:"product_id"`
	Barcode   string  `json:"barcode"`
	Name      string  `json:"name"`
	Quantity  float64 `json:"quantity"`
	Unitprice float64 `json:"unit_price"`
	Discount  float64 `json:"discount"`
	Gstrate   float64 `json:"gst_rate"`
	Tax       float64 `json:"tax"`
	Linetotal float64 `json:"line_total"`
}

// PosOrderCustomer is the shopper snapshot carried on the bill itself.
//
// Deliberately thin. The full profile travels on its own uplink; this exists so
// a bill can be attached to somebody even when their registration has not
// arrived yet.
type PosOrderCustomer struct {
	Id     string `json:"id"`
	Mobile string `json:"mobile"`
	Name   string `json:"name"`
}

// PosOrderPayment is one tender against a bill. A bill may be split across
// several.
type PosOrderPayment struct {
	Method    string  `json:"method"`
	Amount    float64 `json:"amount"`
	Reference string  `json:"reference"`
}

// PosOrderPromo records a campaign that fired, as an amount rather than a rule.
// A bill read back years later must show what was actually given, not what
// today's rules would give.
type PosOrderPromo struct {
	Id     string  `json:"id"`
	Name   string  `json:"name"`
	Type   string  `json:"type"`
	Amount float64 `json:"amount"`
}

// PosOrder is one completed sale.
//
// Id is a UUID minted at the till and is the only thing that identifies this
// bill. It is what deduplication keys on, because at-least-once delivery means
// the same bill legitimately arrives more than once.
type PosOrder struct {
	Id             string            `json:"id"`
	Invoicenumber  string            `json:"invoice_number"`
	Createdat      string            `json:"created_at"`
	Terminalid     string            `json:"terminal_id"`
	Cashier        string            `json:"cashier"`
	Customer       *PosOrderCustomer `json:"customer"`
	Subtotal       float64           `json:"subtotal"`
	Discount       float64           `json:"discount"`
	Promos         []PosOrderPromo   `json:"promos"`
	Tax            float64           `json:"tax"`
	Roundoff       float64           `json:"round_off"`
	Total          float64           `json:"total"`
	Pointsearned   int               `json:"points_earned"`
	Pointsredeemed int               `json:"points_redeemed"`
	Payments       []PosOrderPayment `json:"payments"`
	Items          []PosOrderItem    `json:"items"`

	// GST per slab, as printed on the tax invoice: {"0.05": 12.30, "0.18": 4.50}.
	// Absent from terminals built before this field existed, which is why every
	// consumer of it has to tolerate an empty map.
	Taxbreakdown map[string]float64 `json:"tax_breakdown"`
}

// PosOrderBatch is the envelope a terminal publishes.
//
// Storeid carries the numeric tenantlocations.locationid as a string. The
// tenant is resolved from it server-side and never taken from the terminal — a
// till must not be able to name the tenant it posts into.
type PosOrderBatch struct {
	Schema     int        `json:"schema"`
	Batchid    string     `json:"batch_id"`
	Storeid    string     `json:"store_id"`
	Terminalid string     `json:"terminal_id"`
	Sentat     string     `json:"sent_at"`
	Orders     []PosOrder `json:"orders"`
}

// PosCustomer is a shopper registered at a till.
//
// No loyalty figures. Points, lifetime spend and visit counts are derived from
// the bill stream, which is idempotent and sees every counter; accepting a
// terminal's local balance would make the last till to sync win.
type PosCustomer struct {
	Id                   string `json:"id"`
	Mobile               string `json:"mobile"`
	Name                 string `json:"name"`
	Email                string `json:"email"`
	Gender               string `json:"gender"`
	Dateofbirth          string `json:"date_of_birth"`
	Registeredat         string `json:"registered_at"`
	Registeredbyterminal string `json:"registered_by_terminal"`
}

type PosCustomerBatch struct {
	Schema     int           `json:"schema"`
	Batchid    string        `json:"batch_id"`
	Storeid    string        `json:"store_id"`
	Terminalid string        `json:"terminal_id"`
	Sentat     string        `json:"sent_at"`
	Customers  []PosCustomer `json:"customers"`
}

// PosAck is the only thing that retires a bill on the terminal.
//
// The rule the whole design rests on: a till marks a record synced if and only
// if its id appears in Accepted. Silence is not acceptance — an empty ack, a
// dropped connection or a 200 with no body all leave the record pending and it
// is sent again.
//
// Naming an id in Rejected is a decision, not a fault: the terminal stops
// retrying that record and waits for a person. Use it for "this bill is
// malformed", never for "the database is having a bad minute" — for the latter,
// do not ack at all and let the till back off and retry.
type PosAck struct {
	Batchid  string            `json:"batch_id"`
	Accepted []string          `json:"accepted"`
	Rejected map[string]string `json:"rejected,omitempty"`
}

// NewPosAck returns an ack with non-nil members, so it serialises as `[]` and
// `{}` rather than `null`. A terminal reading null for accepted would treat the
// whole batch as unconfirmed.
func NewPosAck(batchID string) *PosAck {
	return &PosAck{
		Batchid:  batchID,
		Accepted: make([]string, 0),
		Rejected: make(map[string]string),
	}
}

func (a *PosAck) Accept(id string) {
	a.Accepted = append(a.Accepted, id)
}

func (a *PosAck) Reject(id, reason string) {
	a.Rejected[id] = reason
}

// PosCatalogueProduct is one product as the till stores it.
type PosCatalogueProduct struct {
	Id       string  `json:"id"`
	Name     string  `json:"name"`
	Barcode  string  `json:"barcode"`
	Sku      string  `json:"sku"`
	Category string  `json:"category"`
	Price    float64 `json:"price"`
	Mrp      float64 `json:"mrp,omitempty"`
	Stock    float64 `json:"stock"`
	Unit     string  `json:"unit"`
	Gstrate  float64 `json:"gst_rate"`
	Hsncode  string  `json:"hsn_code,omitempty"`
	Brand    string  `json:"brand,omitempty"`
	Isactive bool    `json:"is_active"`
}

// PosCatalogueCustomer is a shopper travelling *down* to a terminal.
//
// The mirror of PosCustomer, and the difference is the point: the uplink
// carries no loyalty figures because a till's local balance is only its own
// view, while the downlink carries them because the back office has seen every
// counter and is the only thing that can total them.
type PosCatalogueCustomer struct {
	Id            string  `json:"id"`
	Name          string  `json:"name"`
	Mobile        string  `json:"mobile"`
	Email         string  `json:"email,omitempty"`
	Gender        string  `json:"gender,omitempty"`
	Dateofbirth   string  `json:"date_of_birth,omitempty"`
	Loyaltypoints int     `json:"loyalty_points"`
	Lifetimespend float64 `json:"lifetime_spend"`
	Visitcount    int     `json:"visit_count"`
	Createdat     string  `json:"created_at,omitempty"`
	Lastvisitat   string  `json:"last_visit_at,omitempty"`
}

// PosCatalogueResponse answers a terminal's catalogue pull.
//
// Isdelta is load-bearing. A response marked false is treated as a full
// snapshot and the terminal withdraws every product it does not mention — so
// answering a change set with false empties the shelf.
type PosCatalogueResponse struct {
	Revision   string                 `json:"revision"`
	Isdelta    bool                   `json:"is_delta"`
	Hasmore    bool                   `json:"has_more"`
	Products   []PosCatalogueProduct  `json:"products"`
	Customers  []PosCatalogueCustomer `json:"customers"`
	Retiredids []string               `json:"retired_product_ids"`
}
