// refill, buy galon, buy galon plus contents, and borrow galon, fulfilled by
// pickup or delivery. It is an auditable aggregate — never a destructive edit
// to sales rows. Deposit/loan/penalty policy is DEFAULT PENDING CONFIRMATION
// (§20 #3): deposit_amount defaults to 0 until the business confirms.
package galon

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"time"

	"github.com/shopspring/decimal"

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

var (
	ErrNotFound      = errors.New("not found")
	ErrInvalidStatus = errors.New("invalid galon transaction status")
)

// Transaction types confirmed in §14.
const (
	TypeRefill              = "refill"
	TypeBuyGalon            = "buy_galon"
	TypeBuyGalonPlusContent = "buy_galon_plus_contents"
	TypeBorrowGalon         = "borrow_galon"
)

// Fulfillment modes confirmed in §14.
const (
	FulfillmentPickup   = "pickup"
	FulfillmentDelivery = "delivery"
)

// InventoryStatus is the POS stat-tile payload. The JSON keys are the ones the
// POS tiles read and must not be renamed: src/pages/pos/POSPage.vue renders
// galonStatus?.diToko, .dipinjam and .pengiriman.
//
// AtStore is *int64 and always nil today. "Galons held at the store" is a
// per-unit state and there is no galon unit registry to read it from — no such
// table, gallon_code NULL on every row, no unit status column. It is null
// rather than 0 because 0 is an answer: it says the store holds no galons,
// which is a claim about the world this database cannot make. The POS template
// renders null as "—" via `?? '—'`, which is the honest display.
type InventoryStatus struct {
	AtStore    *int64 `json:"diToko"`
	OnLoan     int64  `json:"dipinjam"`
	InDelivery int64  `json:"pengiriman"`
}

// Transaction mirrors galon_transactions.
type Transaction struct {
	ID                int64           `json:"id"`
	TransactionNumber string          `json:"transaction_number"`
	CustomerID        int64           `json:"customer_id"`
	Type              string          `json:"type"`
	FulfillmentMode   string          `json:"fulfillment_mode"`
	ProductID         *int64          `json:"product_id"`
	Quantity          decimal.Decimal `json:"quantity"`
	UnitPrice         decimal.Decimal `json:"unit_price"`
	DepositAmount     decimal.Decimal `json:"deposit_amount"`
	LoanAmount        decimal.Decimal `json:"loan_amount"`
	LoanReturned      bool            `json:"loan_returned"`
	Status            string          `json:"status"`
	Notes             *string         `json:"notes"`
	CreatedBy         *int64          `json:"created_by"`
	// StoreID is the tenant binding (§5.4): derived from the caller's
	// identity for restricted users, optional for platform-wide callers.
	StoreID    *int64    `json:"store_id"`
	GallonCode *string   `json:"gallon_code"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
}

// TransactionInput mirrors the create request body.
type TransactionInput struct {
	CustomerID      int64           `json:"customer_id"`
	Type            string          `json:"type"`
	FulfillmentMode string          `json:"fulfillment_mode"`
	ProductID       *int64          `json:"product_id"`
	Quantity        decimal.Decimal `json:"quantity"`
	UnitPrice       decimal.Decimal `json:"unit_price"`
	DepositAmount   decimal.Decimal `json:"deposit_amount"`
	LoanAmount      decimal.Decimal `json:"loan_amount"`
	Notes           *string         `json:"notes"`
	CreatedBy       *int64          `json:"created_by"`
	// StoreID: optional explicit binding; resolved/validated server-side
	// against the caller's tenant scope.
	StoreID *int64 `json:"store_id"`
	// GallonCode: optional physical-gallon barcode identity for tracking.
	GallonCode *string `json:"gallon_code"`
}

type rawTransactionInput struct {
	CustomerID      int64           `json:"customer_id"`
	CustID          int64           `json:"customerId"`
	Type            string          `json:"type"`
	ServiceID       string          `json:"service_id"`
	ServiceIdCamel  string          `json:"serviceId"`
	FulfillmentMode string          `json:"fulfillment_mode"`
	Fulfillment     string          `json:"fulfillment"`
	ProductID       *int64          `json:"product_id"`
	Quantity        decimal.Decimal `json:"quantity"`
	UnitPrice       decimal.Decimal `json:"unit_price"`
	DepositAmount   decimal.Decimal `json:"deposit_amount"`
	Notes           *string         `json:"notes"`
	CreatedBy       *int64          `json:"created_by"`
	GallonCode      *string         `json:"gallon_code"`
	GallonCodeCamel *string         `json:"gallonCode"`
}

func (in *TransactionInput) UnmarshalJSON(data []byte) error {
	var raw rawTransactionInput
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}

	in.CustomerID = raw.CustomerID
	if in.CustomerID == 0 {
		in.CustomerID = raw.CustID
	}
	if in.CustomerID == 0 {
		in.CustomerID = 1
	}

	in.Type = raw.Type
	if in.Type == "" {
		svc := raw.ServiceID
		if svc == "" {
			svc = raw.ServiceIdCamel
		}
		switch svc {
		case "isi_ulang":
			in.Type = TypeRefill
		case "beli_galon":
			in.Type = TypeBuyGalonPlusContent
		case "pinjam_galon":
			in.Type = TypeBorrowGalon
		default:
			in.Type = svc
		}
	}

	in.FulfillmentMode = raw.FulfillmentMode
	if in.FulfillmentMode == "" {
		switch raw.Fulfillment {
		case "antar", "delivery":
			in.FulfillmentMode = FulfillmentDelivery
		default:
			in.FulfillmentMode = FulfillmentPickup
		}
	}

	in.ProductID = raw.ProductID
	in.Quantity = raw.Quantity
	if in.Quantity.IsZero() || in.Quantity.IsNegative() {
		in.Quantity = decimal.NewFromInt(1)
	}
	in.UnitPrice = raw.UnitPrice
	in.DepositAmount = raw.DepositAmount
	in.Notes = raw.Notes
	in.CreatedBy = raw.CreatedBy
	in.GallonCode = raw.GallonCode
	if in.GallonCode == nil {
		in.GallonCode = raw.GallonCodeCamel
	}
	if in.GallonCode != nil {
		if c := strings.ToUpper(strings.TrimSpace(*in.GallonCode)); c == "" {
			in.GallonCode = nil
		} else {
			in.GallonCode = &c
		}
	}
	return nil

}

// Filter carries pagination and filters for List.
type Filter struct {
	Page       int
	PerPage    int
	Status     string
	Type       string
	CustomerID int64
	GallonCode string
}

// GallonInfo is the barcode-tracker summary for one physical gallon.
type GallonInfo struct {
	Code         string     `json:"code"`
	RefillCount  int64      `json:"refill_count"`
	LastRefillAt *time.Time `json:"last_refill_at"`
}

// Seq allocates atomic counters (pkg/seq) — injected so tests fake it.
type Seq interface {
	Sequence(ctx context.Context, prefix string, date time.Time, tx any) (int64, error)
}

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

// Store is the galon persistence seam (sqlc-backed).
type Store interface {
	Begin(ctx context.Context) (Tx, error)
	CreateTransaction(ctx context.Context, q db.DBTX, p db.CreateGalonTransactionParams) (db.GalonTransaction, error)
	SetGalonLoan(ctx context.Context, q db.DBTX, p db.SetGalonLoanParams) (db.GalonTransaction, error)
	GetByID(ctx context.Context, q db.DBTX, id int64) (db.GalonTransaction, error)
	GetByIDForUpdate(ctx context.Context, q db.DBTX, id int64) (db.GalonTransaction, error)
	SetStatus(ctx context.Context, q db.DBTX, id int64, status string) error
	List(ctx context.Context, q db.DBTX, f db.ListGalonTransactionsParams) ([]db.GalonTransaction, error)
	Count(ctx context.Context, q db.DBTX, f db.CountGalonTransactionsParams) (int64, error)

	// barcode tracking
	GallonInfoByCode(ctx context.Context, q db.DBTX, code string, storeIDs []int64) (*GallonInfo, error)
	TransactionsByCode(ctx context.Context, q db.DBTX, code string, storeIDs []int64) ([]db.GalonTransaction, error)

	// lookups
	CustomerExists(ctx context.Context, q db.DBTX, id int64) (bool, error)
	ProductExists(ctx context.Context, q db.DBTX, id int64) (bool, error)

	// InventoryStatus backs the POS stat tiles. It takes the caller's allowed
	// stores rather than a Filter, because the three tiles are three different
	// predicates rather than one listing — see db/query/galon.sql.
	InventoryStatus(ctx context.Context, q db.DBTX, allowedStoreIDs []int64) (db.GalonInventoryStatusRow, error)
}

// QRISResult represents the generated Midtrans QR code for a galon transaction.
type QRISResult struct {
	TransactionID string    `json:"transaction_id"`
	QRURL         string    `json:"qr_url"`
	QRString      string    `json:"qr_string,omitempty"`
	ExpiresAt     time.Time `json:"expires_at"`
}

// GalonPaymentStatusResult is the polling result for till deferred-QRIS settlement.
type GalonPaymentStatusResult struct {
	OrderStatus   string       `json:"order_status"`
	PaymentStatus string       `json:"payment_status"`
	Transaction   *Transaction `json:"transaction,omitempty"`
}

// QRISGateway is the interface for charging and checking Midtrans QRIS status.
type QRISGateway interface {
	ChargeQRIS(ctx context.Context, orderRef string, gross decimal.Decimal) (*QRISResult, error)
	CheckQRISStatus(ctx context.Context, orderRef string) (action string, txnID string, err error)
}
