package models

import "strings"

// 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"`
}

// ---------------------------------------------------------------- Sign-in
//
// A terminal used to hold a store id typed into Settings and a password
// compiled into the app. That made the store id a *claim* rather than a fact:
// any till could name any outlet and be believed, and one leaked build opened
// every tenant on the platform.
//
// These types replace it with the account model the web console already uses.
// A person signs in with their own `app_users` credentials, and the outlet
// comes out of their record instead of going in from the wire.

// PosLoginRequest is what a till sends to sign in.
//
// Authname or Contactno, matching the web console's own login — a shop should
// not need a second set of credentials just because the screen is a till.
//
// Locationid is optional and only means anything for a user entitled to more
// than one outlet: it says which of theirs this terminal is standing in. It is
// checked against what they may reach, never trusted on its own.
type PosLoginRequest struct {
	Authname   string `json:"authname"`
	Contactno  string `json:"contactno"`
	Password   string `json:"password"`
	Configid   int    `json:"configid"`
	Locationid int    `json:"location_id"`

	// A PIN, for signing on at a terminal a supervisor has already opened. Only
	// honoured by the PIN route, which requires an existing session — four
	// digits is no barrier to an anonymous caller.
	Pin string `json:"pin"`

	// Which physical till is asking. Recorded on the session so a stolen token
	// can be told apart from the terminal it was issued to.
	Terminalid string `json:"terminal_id"`
	Deviceid   string `json:"device_id"`
}

// PosLoginLocation is one outlet a signed-in user may bill for.
type PosLoginLocation struct {
	Locationid   int    `json:"location_id"`
	Locationname string `json:"location_name"`
	Address      string `json:"address,omitempty"`
	City         string `json:"city,omitempty"`
	Status       string `json:"status,omitempty"`
}

// PosSession is what a till holds for the rest of the trading day.
//
// Storeid is returned as a string because that is the shape the terminal's
// configuration already stores and sends — handing it back in the form it will
// be replayed in removes a conversion, and a conversion is where a store id
// gets mangled.
type PosSession struct {
	Token     string `json:"token"`
	Expiresat string `json:"expires_at"`

	Userid   int    `json:"user_id"`
	Fullname string `json:"full_name"`
	Email    string `json:"email,omitempty"`
	Roleid   int    `json:"role_id"`

	// What the role is called, and the one thing the terminal actually branches
	// on. Sent as a flag rather than leaving the till to map role ids itself:
	// `app_roles` has six rows for four roles and most accounts carry an id
	// absent from it, so any mapping written on the terminal would be wrong.
	Role           string `json:"role"`
	Canmanagestaff bool   `json:"can_manage_staff"`

	// Which portal this account belongs to. Carried so a supervisor creating a
	// cashier gives them the same configid — an account created under the wrong
	// one cannot sign into the web console and is invisible to half the
	// platform's queries. Not sent to the terminal: it has no use for it and it
	// is one more number to get wrong.
	Configid int `json:"-"`

	Tenantid   int    `json:"tenant_id"`
	Tenantname string `json:"tenant_name"`

	Storeid      string `json:"store_id"`
	Locationid   int    `json:"location_id"`
	Locationname string `json:"location_name"`
	Gstin        string `json:"gstin,omitempty"`
	Address      string `json:"address,omitempty"`
	Phone        string `json:"phone,omitempty"`

	// Every outlet this account may sign a terminal into. A single-outlet user
	// gets a list of one, so the till has no special case: it shows a picker
	// when there is a choice and skips it when there is not.
	Locations []PosLoginLocation `json:"locations"`

	// The people who may ring a bill at the chosen outlet.
	//
	// Sent with the session so a terminal is ready to trade the moment it signs
	// in, rather than needing a second call before the first customer. May be
	// empty — most tenants have no staff recorded yet — and the terminal has to
	// cope with that rather than treat it as a failure.
	Staff []PosStaffMember `json:"staff"`
}

// PosStaffMember is one person who may ring a bill at an outlet.
//
// Distinct from the account that signs the *terminal* in. The sign-in says
// which shop this till belongs to; this says who is standing at it, and it is
// what gets stamped on a bill as `cashiername` and settled against at the end
// of a shift.
//
// The PIN travels in the clear, over TLS, and that is a considered choice
// rather than an oversight. A four-digit PIN is brute-forceable in microseconds
// whatever it is wrapped in, so hashing it here would buy the appearance of
// strength and not the substance. What it would cost is real: the terminal
// salts every PIN with its own random salt before storing it, so a hash
// computed here could never be verified there without inventing a shared
// scheme and keeping two codebases agreeing about it for ever.
//
// The honest framing is that a PIN is *shift attribution*, not a security
// boundary. The boundary is the session token — which is what stops a till
// reaching another tenant's books at all. The PIN decides which of the people
// already inside a shop gets credited with a sale, and the terminal still
// stores it hashed at rest.
type PosStaffMember struct {
	Userid   int    `json:"user_id"`
	Fullname string `json:"full_name"`
	Role     string `json:"role"`
	Pin      string `json:"pin,omitempty"`
	Status   string `json:"status,omitempty"`
}

// PosStaffResponse answers a request for an outlet's people.
type PosStaffResponse struct {
	Locationid int              `json:"location_id"`
	Staff      []PosStaffMember `json:"staff"`
}

// ------------------------------------------------------------ POS staff roles
//
// `app_roles` is keyed by roleid and carries a configid, so the same name
// appears more than once — Admin is both 3 and 5, Manager both 4 and 6, one per
// portal. These two are deliberately not per-portal: a till is a till whichever
// tenant owns it, and a role that had to be duplicated per config would be one
// more thing to remember when a tenant is onboarded.
//
// The ids are fixed rather than allocated, because they are referenced from the
// terminal and from this source. `app_roles.roleid` has no sequence and no
// default — every id in that table was assigned by hand — so nothing is being
// worked around here.
const (
	// PosRoleSupervisor runs the terminal: settings, imports, price overrides,
	// voids, and creating the people below.
	PosRoleSupervisor = 7

	// PosRoleCashier bills, and nothing else.
	PosRoleCashier = 8
)

// PosRoleName maps a role id to what a person calls it.
func PosRoleName(roleID int) string {
	switch roleID {
	case PosRoleSupervisor:
		return "Supervisor"
	case PosRoleCashier:
		return "Cashier"
	}
	return ""
}

// PosRoleFromName reads the role off a request.
//
// Accepts the name rather than the number, so a caller never has to hardcode 7
// or 8 — and returns 0 for anything unrecognised, which every caller treats as
// a refusal rather than as a default.
func PosRoleFromName(name string) int {
	switch strings.ToLower(strings.TrimSpace(name)) {
	case "supervisor":
		return PosRoleSupervisor
	case "cashier":
		return PosRoleCashier
	}
	return 0
}

// PosRoleEligible reports whether a role may open a till at all.
//
// The terminal and the Nearle Daily application share one `app_users` table,
// and that is the only thing they share. An account belongs to one product or
// the other and never to both: a person who administers a shop from a browser
// does not thereby get a cash drawer, and a cashier does not thereby get the
// back office.
//
// Eligibility is therefore granted explicitly — by provisioning a Supervisor or
// a Cashier from the console — and is never inherited from a back-office role.
// Anything else is refused at sign-in, including roleid 0, which is not a role
// but the absence of one.
func PosRoleEligible(roleID int) bool {
	return roleID == PosRoleSupervisor || roleID == PosRoleCashier
}

// PosRoleCanManageStaff reports whether a role may create and edit till users.
//
// Supervisors, and nobody else.
//
// This used to include the back office's own roles 1 to 6, on the reasoning
// that somebody who can already administer a shop from a browser is not made
// less privileged by standing at the counter. That was wrong, and live data
// showed how wrong: it handed till-supervisor powers to 68 accounts, 59 of them
// Nearle Daily Super admins, not one of whom is the administrator of anybody's
// POS. The actual shop accounts carry roleid 0 and were refused.
//
// The back office reaches the till by *provisioning* a supervisor from the
// console, not by becoming one at the counter.
func PosRoleCanManageStaff(roleID int) bool {
	return roleID == PosRoleSupervisor
}

// PosUser is a person who signs in at a till.
type PosUser struct {
	Userid     int    `json:"user_id"`
	Fullname   string `json:"full_name"`
	Firstname  string `json:"first_name,omitempty"`
	Lastname   string `json:"last_name,omitempty"`
	Authname   string `json:"authname,omitempty"`
	Contactno  string `json:"contactno,omitempty"`
	Roleid     int    `json:"role_id"`
	Role       string `json:"role"`
	Pin        string `json:"pin,omitempty"`
	Haspassword bool  `json:"has_password"`
	Locationid int    `json:"location_id"`
	Status     string `json:"status"`
}

// PosUserRequest creates or edits a till user.
//
// Note what is absent: tenant and location. Both come from the caller's own
// session token. A supervisor creating staff can only ever create them at their
// own outlet, and no field in this struct can say otherwise — which is the same
// inversion that stopped a till naming its own shop.
type PosUserRequest struct {
	Userid    int    `json:"user_id"`
	Fullname  string `json:"full_name"`
	Role      string `json:"role"`
	Pin       string `json:"pin"`
	Password  string `json:"password"`
	Authname  string `json:"authname"`
	Contactno string `json:"contactno"`
	Status    string `json:"status"`
}

// PosUserWebRequest is a staff change made from the web console.
//
// Identical to [PosUserRequest] but for the two fields a terminal never needs
// to send: the console has no session token, so it has to name the outlet it is
// working on. That is the one real difference between the two doors into this,
// and it is also the weaker one — the till's outlet is proved by a signature,
// while this is asserted. The handler checks the outlet belongs to the tenant
// before writing anything, which is as far as it can go without the console
// holding a session of its own.
type PosUserWebRequest struct {
	PosUserRequest
	Tenantid   int `json:"tenantid"`
	Locationid int `json:"locationid"`
}
