// Package lhpp implements Laporan Harian Penagihan Piutang: a printable billing
// sheet a collector works from. A group is PERSISTED (lhpp_groups +
// lhpp_group_items) and maintained by hand, not computed on read:
//
//   - every dispatched delivery order gets its own single-DO group on dispatch
//     (see the distribution LHPPGrouper seam) — nothing is batched together
//     automatically, so a collector can still see unbatched work;
//   - a human merges sheets into one group and can split them again, so grouping
//     is reversible;
//   - one group is always one driver, and may span several days: date_from /
//     date_to are derived on read from the members' do_date, never stored,
//     because they change on every merge.
//
// A group's LHPPNumber is allocated once (LHPP-YYYYMM-NNNNNN, from
// document_number_sequences inside the creating transaction) and then read
// straight from the row — never recomputed, so a printed sheet keeps the number
// it was printed with.
//
// Money does not depend on grouping: per-item figures come from the invoice,
// totals are a sum over items, and payments post through
// finance.Service.PayInvoice — the exact path ReceivablesPage uses — so LHPP
// and ReceivablesPage can never drift apart, and regrouping changes bucket
// count without changing an amount.
package lhpp

import "github.com/shopspring/decimal"

// LHPP sheet status values.
const (
	// StatusBelumDitagih means nothing is collectable yet because invoicing
	// is still pending (the DO has no invoice at all).
	StatusBelumDitagih = "belum_ditagih"
	// StatusSebagian means part of the billed total has been paid.
	StatusSebagian = "sebagian"
	// StatusLunas means every live invoice in the group is fully paid.
	StatusLunas = "lunas"
	// StatusDibatalkan means every DO in the group had its invoice written
	// off (invoices.status = 'cancelled'). Cancelled invoices contribute
	// zero to Tagihan/Terbayar/Sisa, so a group of only cancellations is
	// neither collectable ("lunas"/"sebagian") nor awaiting invoicing
	// ("belum_ditagih") — it is a written-off debt. Driver LHPP only: the
	// Sales LHPP query filters cancelled invoices out of the join, so it can
	// never produce this status.
	StatusDibatalkan = "dibatalkan"
)

// lhpp_groups.status values. This is a LIFECYCLE state, orthogonal to the
// billing Status* above: a voided group is not a payment state, it is a retired
// sheet whose number stays auditable.
const (
	GroupActive = "active"
	GroupVoided = "voided"
)

// lhpp_group_history.action values.
const (
	ActionCreated       = "created"
	ActionMerged        = "merged"
	ActionSplit         = "split"
	ActionVoided        = "voided"
	ActionDriverChanged = "driver_changed"
	ActionAssigned      = "assigned"
)

// lhpp_group_items_do_unique — the name encoding "a delivery order is in at
// most one group, ever", matched on pgconn.PgError.ConstraintName so a losing
// concurrent merge becomes 409 instead of a 500. The NAME is the contract, not
// the constraint-vs-index shape: PostgreSQL reports the violating index's name
// either way, so renaming this silently downgrades a 409 into a 500.
const constraintDOUnique = "lhpp_group_items_do_unique"

// Item is one delivery order's billing line inside an LHPP group.
type Item struct {
	DOID            int64           `json:"do_id"`
	DONumber        string          `json:"do_number"`
	CustomerName    string          `json:"customer_name"`
	ShippingAddress string          `json:"shipping_address"`
	InvoiceID       *int64          `json:"invoice_id"`
	InvoiceNumber   *string         `json:"invoice_number"`
	Tagihan         decimal.Decimal `json:"tagihan"`
	Terbayar        decimal.Decimal `json:"terbayar"`
	Sisa            decimal.Decimal `json:"sisa"`

	// DepositStatus tracks this item's cash/giro payment(s) through the
	// Setoran Tunai Driver custody chain — populated on demand (see
	// enrichDeposit), independent of Terbayar/piutang status above:
	//   ""              no cash/giro payment recorded for this item
	//   "belum_disetor" a cash/giro payment exists but isn't in any Setoran yet
	//   "pending"       in a Setoran, not yet confirmed received by cashier
	//   "received"      cashier has confirmed receipt
	// When an item has multiple cash/giro payments in different states, the
	// worst case wins (belum_disetor > pending > received).
	DepositStatus string `json:"deposit_status,omitempty"`
}

// Group is one LHPP row: a manually maintained billing sheet holding one or
// more delivery orders for a single driver, with the aggregate billing totals
// sales/collector need to work from. Status is the billing status (one of
// StatusBelumDitagih, StatusSebagian, StatusLunas or StatusDibatalkan);
// GroupStatus is the lifecycle state (GroupActive / GroupVoided).
type Group struct {
	ID            int64           `json:"id"`
	LHPPNumber    string          `json:"lhpp_number"`
	LHPPDate      string          `json:"lhpp_date"`
	DateFrom      string          `json:"date_from"`
	DateTo        string          `json:"date_to"`
	DriverID      int64           `json:"driver_id"`
	DriverName    string          `json:"driver_name"`
	FleetName     string          `json:"fleet_name"`
	PlateNumber   string          `json:"plate_number"`
	DOCount       int             `json:"do_count"`
	TotalTagihan  decimal.Decimal `json:"total_tagihan"`
	TotalTerbayar decimal.Decimal `json:"total_terbayar"`
	TotalSisa     decimal.Decimal `json:"total_sisa"`
	Status        string          `json:"status"`
	GroupStatus   string          `json:"group_status"`
	Items         []Item          `json:"items,omitempty"`

	// Sheet handover (000149): the user currently carrying the sheet to
	// collect with — "admin piutang memberi LHPP kepada sales". NULL means
	// the sheet is back at the office / not yet handed out. Handovers append
	// 'assigned' rows to lhpp_group_history; receiving money is NOT this
	// feature (payments post through finance.PayInvoice as before).
	AssignedTo   *int64 `json:"assigned_to,omitempty"`
	AssigneeName string `json:"assignee_name,omitempty"`
	AssignedAt   string `json:"assigned_at,omitempty"`

	// DepositStatus rolls up Items[*].DepositStatus (worst case) — populated
	// on demand for List's current page and Get's single group only, see
	// enrichDeposit, so paginated history doesn't pay for an extra query per
	// row. Empty when no item in this group has a cash/giro payment.
	DepositStatus string `json:"deposit_status,omitempty"`
}

// MergeInput is the POST /lhpp/groups/merge body. All listed groups must belong
// to the same driver (one group is one driver) and at least two are required —
// merging a group into itself would be a no-op pretending to be an action.
// TargetGroupID names the surviving sheet; when zero the lowest id wins, and it
// must be one of GroupIDs. The target keeps its LHPPNumber; every source that
// ends up empty is voided.
type MergeInput struct {
	GroupIDs      []int64 `json:"group_ids"`
	TargetGroupID int64   `json:"target_group_id"`
	Reason        string  `json:"reason"`
}

// SplitInput is the POST /lhpp/groups/{id}/split body. The named DOs move into
// a NEW group (new number, allocated in the same transaction), leaving the
// source sheet — and its number — intact. A source left with no members is
// voided. Splitting a group into itself is refused: that is a void.
type SplitInput struct {
	DOIDs  []int64 `json:"do_ids"`
	Reason string  `json:"reason"`
}

// VoidInput is the POST /lhpp/groups/{id}/void body. Voiding deletes the
// membership rows, which is what releases those DOs for re-batching.
type VoidInput struct {
	Reason string `json:"reason"`
}

// UpdateGroupInput is the PATCH /lhpp/groups/{id} body. Only the driver is
// mutable, and only while the sheet holds a single DO: driver_id is immutable
// once the group has more than one member, because the sheet a collector is
// carrying is addressed by driver.
type UpdateGroupInput struct {
	DriverID int64  `json:"driver_id"`
	Reason   string `json:"reason"`
}

// HistoryEntry is one lhpp_group_history row.
type HistoryEntry struct {
	ID          int64  `json:"id"`
	DOID        *int64 `json:"do_id"`
	Action      string `json:"action"`
	FromGroupID *int64 `json:"from_group_id"`
	ToGroupID   *int64 `json:"to_group_id"`
	FromDriver  *int64 `json:"from_driver_id"`
	ToDriver    *int64 `json:"to_driver_id"`
	Reason      string `json:"reason"`
	ChangedBy   *int64 `json:"changed_by"`
}

// ListFilter narrows GET /lhpp.
type ListFilter struct {
	Page    int
	PerPage int
	Search  string
	Status  string
}

// PaymentInput is the POST /lhpp/{id}/payments body.
type PaymentInput struct {
	DOID          int64           `json:"do_id"`
	Amount        decimal.Decimal `json:"amount"`
	PaymentMethod string          `json:"payment_method"`
	Notes         *string         `json:"notes"`
}

// SalesItem is one open invoice a billing visit is meant to collect —
// analogous to Item, but keyed by customer + invoice instead of a delivery
// order (a penagihan visit has no DO; the sales rep collects against the
// customer's open invoices directly).
type SalesItem struct {
	VisitID         int64           `json:"visit_id"`
	CustomerID      int64           `json:"customer_id"`
	CustomerName    string          `json:"customer_name"`
	ShippingAddress string          `json:"shipping_address"`
	Purpose         string          `json:"purpose"`
	InvoiceID       *int64          `json:"invoice_id"`
	InvoiceNumber   *string         `json:"invoice_number"`
	Tagihan         decimal.Decimal `json:"tagihan"`
	Terbayar        decimal.Decimal `json:"terbayar"`
	Sisa            decimal.Decimal `json:"sisa"`
}

// SalesGroup is one Sales LHPP row: every billing visit scheduled for the
// same sales rep on the same date.
type SalesGroup struct {
	ID            int64           `json:"id"`
	LHPPNumber    string          `json:"lhpp_number"`
	LHPPDate      string          `json:"lhpp_date"`
	SalesID       int64           `json:"sales_id"`
	SalesName     string          `json:"sales_name"`
	VisitCount    int             `json:"visit_count"`
	TotalTagihan  decimal.Decimal `json:"total_tagihan"`
	TotalTerbayar decimal.Decimal `json:"total_terbayar"`
	TotalSisa     decimal.Decimal `json:"total_sisa"`
	Status        string          `json:"status"`
	Items         []SalesItem     `json:"items,omitempty"`
}

// SalesListFilter narrows GET /lhpp/sales. SalesID restricts to one sales
// rep's own groups (set from the authenticated caller on GET /lhpp/sales/me).
type SalesListFilter struct {
	Page    int
	PerPage int
	Search  string
	Status  string
	SalesID *int64
}

// SalesPaymentInput is the POST /lhpp/sales/{id}/payments body. Matched by
// invoice_id rather than do_id — a billing visit has no delivery order, the
// invoice itself is the payable unit.
type SalesPaymentInput struct {
	InvoiceID     int64           `json:"invoice_id"`
	Amount        decimal.Decimal `json:"amount"`
	PaymentMethod string          `json:"payment_method"`
	Notes         *string         `json:"notes"`
}
