// Package distribution implements DO-based partial fulfillment
// (business-rules §15, migration-plan Phase 4). A Delivery Order is the parent
// fulfillment document for one Sales Order; one or more Shipment executions
// deduct stock by the actual shipped quantity only (never the full request).
// Codes: DO-YYYYMM-000001 and SHP-YYYYMM-000001 (§17). All writes inside a use
// case run on the caller's tx; repository reads use the pool.
package distribution

import (
	"context"
	"time"

	"github.com/shopspring/decimal"

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

// Delivery statuses (business-rules §15.2). Transitions are enforced by the
// service, not merely represented as CHECK values.
const (
	StatusDraft              = "draft"
	StatusApproved           = "approved"
	StatusPartiallyFulfilled = "partially_fulfilled"
	StatusReady              = "ready"
	StatusInTransit          = "in_transit"
	StatusDelivered          = "delivered"
	StatusCompleted          = "completed"
	StatusCancelled          = "cancelled"
)

// Shipment statuses.
const (
	ShipmentPending   = "pending"
	ShipmentInTransit = "in_transit"
	ShipmentDelivered = "delivered"
	ShipmentFailed    = "failed"
	ShipmentCancelled = "cancelled"
)

// Dispatch statuses.
const (
	DispatchQueued     = "queued"
	DispatchAssigned   = "assigned"
	DispatchDispatched = "dispatched"
	DispatchCancelled  = "cancelled"
)

// Priorities (business-rules §15.4).
const (
	PriorityUrgent = "urgent"
	PriorityHigh   = "high"
	PriorityNormal = "normal"
	PriorityLow    = "low"
)

// DeliveryOrder mirrors delivery_orders.
type DeliveryOrder struct {
	ID                int64           `json:"id"`
	DoNumber          string          `json:"do_number"`
	SalesOrderID      int64           `json:"sales_order_id"`
	CustomerID        int64           `json:"customer_id"`
	WarehouseID       int64           `json:"warehouse_id"`
	DoDate            string          `json:"do_date"`
	Priority          string          `json:"priority"`
	Status            string          `json:"status"`
	RequestedQuantity decimal.Decimal `json:"requested_quantity"`
	ShippedQuantity   decimal.Decimal `json:"shipped_quantity"`
	DeliveredQuantity decimal.Decimal `json:"delivered_quantity"`
	RemainingQuantity decimal.Decimal `json:"remaining_quantity"`
	Notes             *string         `json:"notes"`
	CreatedBy         *int64          `json:"created_by"`
	// Display fields resolved through the sales order / customer joins
	// (so_number, sales_name via users.sales_id — the same relation the
	// sales-orders serializer uses; recipient from the customer's contact
	// person, falling back to the customer name; delivered_at from the
	// latest delivered/completed delivery_status_history row).
	CustomerName string `json:"customer_name,omitempty"`
	SoNumber     string `json:"so_number,omitempty"`
	// SalesOrderStatus is the linked sales order's fulfilment status
	// (draft/pending_approval/confirmed/picking/shipped/delivered/...).
	// "Sedang Dikemas" (picking) lives at the sales-order level — the DO row
	// itself stays `draft` until the first shipment allocates quantity — so
	// the ERP progress stepper reads this field to light the packing step.
	SalesOrderStatus string  `json:"sales_order_status,omitempty"`
	ShippingAddress  *string `json:"shipping_address"`
	SalesName        *string `json:"sales_name"`
	RecipientName    string  `json:"recipient_name,omitempty"`
	DeliveredAt      *string `json:"delivered_at"`
	// Proof of Delivery (000143): photo the driver/warehouse uploads at the
	// handover, plus the recipient name they recorded. Nil/empty when the DO
	// was completed without a photo — evidence, not a gate.
	ProofPhotoURL *string             `json:"proof_photo_url,omitempty"`
	PodRecipient  *string             `json:"pod_recipient_name,omitempty"`
	Items         []DeliveryOrderItem `json:"items,omitempty"`
	Shipments     []Shipment          `json:"shipments,omitempty"`
	Dispatches    []Dispatch          `json:"dispatches,omitempty"`
	CreatedAt     time.Time           `json:"created_at"`
	UpdatedAt     time.Time           `json:"updated_at"`
}

// DeliveryOrderItem mirrors delivery_order_items (NUMERIC(12,2)).
type DeliveryOrderItem struct {
	ID                int64           `json:"id"`
	DeliveryOrderID   int64           `json:"delivery_order_id"`
	ProductID         int64           `json:"product_id"`
	RequestedQuantity decimal.Decimal `json:"requested_quantity"`
	ShippedQuantity   decimal.Decimal `json:"shipped_quantity"`
	DeliveredQuantity decimal.Decimal `json:"delivered_quantity"`
	RemainingQuantity decimal.Decimal `json:"remaining_quantity"`
	ProductCode       string          `json:"product_code,omitempty"`
	ProductName       string          `json:"product_name,omitempty"`
	UnitPrice         decimal.Decimal `json:"unit_price"`
	Subtotal          decimal.Decimal `json:"subtotal"`
}

// Shipment mirrors shipments; its number is distinct from the parent DO number
// (§15.1). tracking_number (TRK-YYYYMM-%06d) and barcode (BAR-... derived from
// the shipment number) are the Phase 5 tracking identifiers (§16).
type Shipment struct {
	ID              int64          `json:"id"`
	ShipmentNumber  string         `json:"shipment_number"`
	TrackingNumber  string         `json:"tracking_number"`
	Barcode         string         `json:"barcode"`
	DeliveryOrderID int64          `json:"delivery_order_id"`
	Status          string         `json:"status"`
	ShipDate        string         `json:"ship_date"`
	Notes           *string        `json:"notes"`
	CreatedBy       *int64         `json:"created_by"`
	Items           []ShipmentItem `json:"items,omitempty"`
	CreatedAt       time.Time      `json:"created_at"`
	UpdatedAt       time.Time      `json:"updated_at"`
}

// ShipmentItem mirrors shipment_items.
type ShipmentItem struct {
	ID                  int64           `json:"id"`
	ShipmentID          int64           `json:"shipment_id"`
	DeliveryOrderItemID int64           `json:"delivery_order_item_id"`
	ProductID           int64           `json:"product_id"`
	ShippedQuantity     decimal.Decimal `json:"shipped_quantity"`
	DeliveredQuantity   decimal.Decimal `json:"delivered_quantity"`
	ProductCode         string          `json:"product_code,omitempty"`
	ProductName         string          `json:"product_name,omitempty"`
}

// Dispatch mirrors dispatches — the operational workflow (priority, queue
// order, fleet, optional driver, schedule, dispatcher, timestamps, §15.4).
type Dispatch struct {
	ID              int64     `json:"id"`
	DeliveryOrderID int64     `json:"delivery_order_id"`
	ShipmentID      int64     `json:"shipment_id"`
	FleetID         int64     `json:"fleet_id"`
	Priority        string    `json:"priority"`
	QueueOrder      int32     `json:"queue_order"`
	DriverID        *int64    `json:"driver_id"`
	ScheduledFor    *string   `json:"scheduled_for"`
	Status          string    `json:"status"`
	DispatchedAt    *string   `json:"dispatched_at"`
	DispatcherID    *int64    `json:"dispatcher_id"`
	CreatedAt       time.Time `json:"created_at"`
	UpdatedAt       time.Time `json:"updated_at"`
}

// Vehicle mirrors fleet (capacity enforcement is REQUIRES BUSINESS
// CONFIRMATION, §15.4 — capacity is stored but not enforced).
// DriverOption is one candidate for the dispatch modal's driver picker.
type DriverOption struct {
	ID   int64  `json:"id"`
	Name string `json:"name"`
}

type Vehicle struct {
	ID             int64            `json:"id"`
	PlateNumber    string           `json:"plate_number"`
	VehicleName    string           `json:"vehicle_name"`
	VehicleType    *string          `json:"vehicle_type"`
	CapacityWeight *decimal.Decimal `json:"capacity_weight"`
	CapacityVolume *decimal.Decimal `json:"capacity_volume"`
	IsActive       bool             `json:"is_active"`
	CreatedAt      time.Time        `json:"created_at"`
	UpdatedAt      time.Time        `json:"updated_at"`
}

// DeliveryStatusHistory mirrors delivery_status_history (append-only audit).
type DeliveryStatusHistory struct {
	ID              int64     `json:"id"`
	DeliveryOrderID int64     `json:"delivery_order_id"`
	FromStatus      string    `json:"from_status"`
	ToStatus        string    `json:"to_status"`
	Reason          string    `json:"reason"`
	ReferenceType   *string   `json:"reference_type"`
	ReferenceID     *int64    `json:"reference_id"`
	ChangedBy       *int64    `json:"changed_by"`
	CreatedAt       time.Time `json:"created_at"`
}

// DOFilter carries pagination/search for the DO list.
type DOFilter struct {
	Page         int
	PerPage      int
	Search       string
	Status       string
	SalesOrderID int64
	CustomerID   int64
	// ERP column sort (sort_by/sort_dir), whitelist-gated in SQL.
	SortBy string
	SortDir string
}

// DOItemInput is one requested line of a new Delivery Order.
type DOItemInput struct {
	ProductID         int64           `json:"product_id"`
	RequestedQuantity decimal.Decimal `json:"requested_quantity"`
}

// DOInput mirrors the create-delivery-order request body.
type DOInput struct {
	SalesOrderID int64         `json:"sales_order_id"`
	WarehouseID  int64         `json:"warehouse_id"`
	Priority     string        `json:"priority"`
	Notes        *string       `json:"notes"`
	Items        []DOItemInput `json:"items"`
}

// ShipmentItemInput is one line of a shipment execution.
type ShipmentItemInput struct {
	ProductID       int64           `json:"product_id"`
	ShippedQuantity decimal.Decimal `json:"shipped_quantity"`
}

// DispatchInfo is the optional dispatch carried by a shipment execution.
type DispatchInfo struct {
	FleetID      int64  `json:"fleet_id"`
	DriverID     *int64 `json:"driver_id"`
	Priority     string `json:"priority"`
	QueueOrder   int32  `json:"queue_order"`
	ScheduledFor string `json:"scheduled_for"`
}

// ShipmentInput mirrors the create-shipment request body.
type ShipmentInput struct {
	ShipDate string              `json:"ship_date"`
	Notes    *string             `json:"notes"`
	Items    []ShipmentItemInput `json:"items"`
	Dispatch *DispatchInfo       `json:"dispatch,omitempty"`
}

// DispatchInput mirrors the dispatch/reassign request body.
type DispatchInput struct {
	ShipmentID   int64  `json:"shipment_id"`
	FleetID      int64  `json:"fleet_id"`
	Priority     string `json:"priority"`
	QueueOrder   int32  `json:"queue_order"`
	DriverID     *int64 `json:"driver_id"`
	ScheduledFor string `json:"scheduled_for"`
}

// VehicleInput mirrors the create-fleet request body.
type VehicleInput struct {
	PlateNumber    string           `json:"plate_number"`
	VehicleName    string           `json:"vehicle_name"`
	VehicleType    *string          `json:"vehicle_type"`
	CapacityWeight *decimal.Decimal `json:"capacity_weight"`
	CapacityVolume *decimal.Decimal `json:"capacity_volume"`
	IsActive       *bool            `json:"is_active"`
}

// FleetFilter carries pagination for the fleet list.
type FleetFilter struct {
	Page     int
	PerPage  int
	IsActive *bool
}

// Tracking event sources (§16).
const (
	TrackingSourceDriverMobile = "driver_mobile"
	TrackingSourceThirdParty   = "third_party"
	TrackingSourceManual       = "manual"
)

// TrackingEvent mirrors shipment_tracking_events — an append-only, timestamped
// record with optional coordinates. Never updated or deleted.
type TrackingEvent struct {
	ID          int64     `json:"id"`
	ShipmentID  int64     `json:"shipment_id"`
	EventTime   time.Time `json:"event_time"`
	Status      string    `json:"status"`
	Source      string    `json:"source"`
	Latitude    *float64  `json:"latitude"`
	Longitude   *float64  `json:"longitude"`
	AccuracyM   *float64  `json:"accuracy_m"`
	SpeedMps    *float64  `json:"speed_mps"`
	HeadingDeg  *float64  `json:"heading_deg"`
	Note        *string   `json:"note"`
	ProviderRef *string   `json:"provider_ref"`
	CreatedBy   *int64    `json:"created_by"`
	CreatedAt   time.Time `json:"created_at"`
}

// TrackingEventInput mirrors the record-tracking-event request body. event_time
// defaults to now when empty; coordinates must be supplied together.
type TrackingEventInput struct {
	EventTime   string   `json:"event_time"`
	Status      string   `json:"status"`
	Source      string   `json:"source"`
	Latitude    *float64 `json:"latitude"`
	Longitude   *float64 `json:"longitude"`
	AccuracyM   *float64 `json:"accuracy_m"`
	SpeedMps    *float64 `json:"speed_mps"`
	HeadingDeg  *float64 `json:"heading_deg"`
	Note        *string  `json:"note"`
	ProviderRef *string  `json:"provider_ref"`
}

// LiveLocation is a point delivered by an active LocationProvider (driver
// mobile GPS or a third-party integration).
type LiveLocation struct {
	Latitude   float64   `json:"latitude"`
	Longitude  float64   `json:"longitude"`
	AccuracyM  float64   `json:"accuracy_m"`
	ObservedAt time.Time `json:"observed_at"`
	Source     string    `json:"source"`
}

// LocationProvider abstracts a live location source (§16). When no provider is
// wired, or Active() is false, the API must not claim real-time tracking.
type LocationProvider interface {
	Name() string
	Active() bool
	Latest(ctx context.Context, shipmentID int64) (*LiveLocation, error)
}

// TrackingPoint is the single "latest" point for a map UI. IsLive is true only
// when it came from an active provider, never from stored history.
type TrackingPoint struct {
	Latitude  float64   `json:"latitude"`
	Longitude float64   `json:"longitude"`
	AccuracyM float64   `json:"accuracy_m"`
	EventTime time.Time `json:"event_time"`
	Source    string    `json:"source"`
	IsLive    bool      `json:"is_live"`
}

// RealTimeInfo exposes whether a live source exists; when Available is false
// every point in the payload is historical.
type RealTimeInfo struct {
	Available bool    `json:"available"`
	Provider  *string `json:"provider"`
}

// TrackingSummary is the map-ready payload: shipment, latest point, history and
// real-time availability.
type TrackingSummary struct {
	Shipment Shipment        `json:"shipment"`
	Latest   *TrackingPoint  `json:"latest"`
	History  []TrackingEvent `json:"history"`
	RealTime RealTimeInfo    `json:"real_time"`
}

// LookupResult resolves any Phase 5 identifier (DO number, shipment number,
// barcode, or tracking number) to shipment tracking summaries.
type LookupResult struct {
	Query     string            `json:"query"`
	MatchedBy string            `json:"matched_by"`
	Shipments []TrackingSummary `json:"shipments"`
}

// Tx is a physical transaction; pgx.Tx satisfies it.
type Tx interface {
	db.DBTX
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

// ShipmentLedger records a stock_out movement for a shipment line inside the
// same tx. before/after come from the same locked stock mutation so the
// movement is exact under concurrency. Wired from main via
// Service.WithShipmentLedger.
type ShipmentLedger func(ctx context.Context, q db.DBTX, productID, warehouseID, shipmentID int64, before, after, qty decimal.Decimal) error

// LHPPGrouper puts a freshly dispatched delivery order onto a billing sheet,
// inside the dispatch transaction and on the q the caller already locked.
// Wired from main via Service.WithLHPPGrouper; the implementation is
// internal/lhpp, which owns the LHPP schema.
//
// It creates exactly ONE single-DO group per dispatch and never buckets by
// (driver, date): grouping is a human decision (see internal/lhpp). Returning
// ErrDOAlreadyBatched refuses the dispatch, which rolls the dispatch row back
// with it — that is the intended outcome, since persisting driver_id on a group
// makes a multi-member sheet's driver a fixed fact.
type LHPPGrouper func(ctx context.Context, q db.DBTX, driverID, doID int64, doDate string) error

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

	// sales order reads and status updates for fulfillment sync
	SalesOrderForDelivery(ctx context.Context, id int64) (db.GetSalesOrderForDeliveryRow, error)
	SalesOrderItems(ctx context.Context, orderID int64) ([]db.ListSalesOrderItemsRow, error)
	SetSalesOrderStatus(ctx context.Context, q db.DBTX, id int64, status string) error

	// delivery orders (q is a tx when the write must be atomic with stock ops)
	CreateDeliveryOrder(ctx context.Context, q db.DBTX, p db.CreateDeliveryOrderParams) (db.CreateDeliveryOrderRow, error)
	DeliveryOrderByID(ctx context.Context, id int64) (db.GetDeliveryOrderByIDRow, error)
	DeliveryOrderByIDForUpdate(ctx context.Context, q db.DBTX, id int64) (db.GetDeliveryOrderByIDForUpdateRow, error)
	ListDeliveryOrders(ctx context.Context, f DOFilter) ([]db.ListDeliveryOrdersRow, int, error)
	UpdateDeliveryOrderTotals(ctx context.Context, q db.DBTX, id int64, shipped, delivered, remaining decimal.Decimal, status string) error
	SetDeliveryOrderStatus(ctx context.Context, q db.DBTX, id int64, status string) error
	// SetDeliveryOrderPODRow writes the proof-of-delivery columns (000143).
	// The photo STORAGE happens in SetDeliveryOrderPOD (pod.go) before this
	// row write, inside the same completion transaction.
	SetDeliveryOrderPODRow(ctx context.Context, q db.DBTX, id int64, photoURL *string, recipient string) error

	// delivery order items
	CreateDeliveryOrderItem(ctx context.Context, q db.DBTX, p db.CreateDeliveryOrderItemParams) (int64, error)
	DeliveryOrderItems(ctx context.Context, orderID int64) ([]db.ListDeliveryOrderItemsRow, error)
	DeliveryOrderItemsForUpdate(ctx context.Context, q db.DBTX, orderID int64) ([]db.DeliveryOrderItem, error)
	UpdateDeliveryOrderItemTotals(ctx context.Context, q db.DBTX, id int64, shipped, delivered, remaining decimal.Decimal) error

	// shipments
	CreateShipment(ctx context.Context, q db.DBTX, p db.CreateShipmentParams) (db.CreateShipmentRow, error)
	ShipmentByID(ctx context.Context, id int64) (db.GetShipmentByIDRow, error)
	ShipmentByIDForUpdate(ctx context.Context, q db.DBTX, id int64) (db.GetShipmentByIDForUpdateRow, error)
	ShipmentsByDeliveryOrder(ctx context.Context, orderID int64) ([]db.ListShipmentsByDeliveryOrderRow, error)
	SetShipmentStatus(ctx context.Context, q db.DBTX, id int64, status string) error
	CreateShipmentItem(ctx context.Context, q db.DBTX, p db.CreateShipmentItemParams) (int64, error)
	ShipmentItems(ctx context.Context, shipmentID int64) ([]db.ListShipmentItemsRow, error)
	ShipmentItemsForUpdate(ctx context.Context, q db.DBTX, shipmentID int64) ([]db.ShipmentItem, error)
	UpdateShipmentItemDelivered(ctx context.Context, q db.DBTX, id int64, delivered decimal.Decimal) error
	ShipmentByShipmentNumber(ctx context.Context, number string) (db.GetShipmentByShipmentNumberRow, error)
	ShipmentByTrackingNumber(ctx context.Context, trackingNumber string) (db.GetShipmentByTrackingNumberRow, error)
	ShipmentByBarcode(ctx context.Context, barcode string) (db.GetShipmentByBarcodeRow, error)
	ShipmentsByDeliveryOrderNumber(ctx context.Context, doNumber string) ([]db.ListShipmentsByDeliveryOrderNumberRow, error)

	// shipment tracking (append-only, §16)
	CreateShipmentTrackingEvent(ctx context.Context, p db.CreateShipmentTrackingEventParams) (db.CreateShipmentTrackingEventRow, error)
	ShipmentTrackingEvents(ctx context.Context, shipmentID int64) ([]TrackingEvent, error)
	ShipmentTrackingEventByProviderRef(ctx context.Context, shipmentID int64, providerRef string) (TrackingEvent, error)
	ShipmentLatestEvent(ctx context.Context, shipmentID int64) (db.ShipmentLatestEventRow, error)
	ActiveShipmentLocations(ctx context.Context) ([]db.ActiveShipmentLocationsRow, error)
	DispatchByShipmentID(ctx context.Context, shipmentID int64) (db.DispatchByShipmentIDRow, error)
	ShipmentCustomerID(ctx context.Context, shipmentID int64) (int64, error)
	CustomerByUserID(ctx context.Context, userID int64) (int64, error)
	ListWarehouseLocations(ctx context.Context) ([]db.ListWarehouseLocationsRow, error)
	ListStoreLocations(ctx context.Context) ([]db.ListStoreLocationsRow, error)
	FleetLatestLocations(ctx context.Context) ([]db.FleetLatestLocationsRow, error)

	// status history (append-only audit, §15.2)
	CreateDeliveryStatusHistory(ctx context.Context, q db.DBTX, p db.CreateDeliveryStatusHistoryParams) (int64, error)
	DeliveryStatusHistory(ctx context.Context, orderID int64) ([]db.DeliveryStatusHistory, error)

	// dispatch (queue order + reassignment are audited, §15.4)
	CreateDispatch(ctx context.Context, q db.DBTX, p db.CreateDispatchParams) (db.CreateDispatchRow, error)
	LatestDispatch(ctx context.Context, q db.DBTX, orderID int64) (db.Dispatch, error)
	DispatchesByDeliveryOrder(ctx context.Context, orderID int64) ([]db.Dispatch, error)

	// fleet
	CreateFleet(ctx context.Context, q db.DBTX, p db.CreateFleetParams) (db.CreateFleetRow, error)
	FleetByID(ctx context.Context, id int64) (db.Fleet, error)
	ListFleet(ctx context.Context, f FleetFilter) ([]db.Fleet, int, error)

	// driver picker for the dispatch modal (any active user; no driver role exists)
	ListActiveUsersForDriverPicker(ctx context.Context) ([]db.ListActiveUsersForDriverPickerRow, error)
	CreateDriverUser(ctx context.Context, p db.CreateDriverUserParams) (db.CreateDriverUserRow, error)

	// stock within a tx: callers lock in deterministic product order before
	// mutating, so concurrent shipments cannot oversell.
	LockStockForUpdate(ctx context.Context, q db.DBTX, productID, warehouseID int64) (db.GetSalesStockRowForUpdateRow, error)
	EnsureStockRow(ctx context.Context, q db.DBTX, productID, warehouseID int64) error
	SetStockQuantity(ctx context.Context, q db.DBTX, stockID int64, quantity, reserved decimal.Decimal) error
}
