package sales

import (
	"fmt"
	"strings"
	"time"

	"github.com/jackc/pgx/v5/pgtype"
	"github.com/shopspring/decimal"

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

// effectivePaymentTerms normalizes display terms: POS (till) orders are always
// cash regardless of stored snapshotted value. Financial rows are immutable.
func effectivePaymentTerms(orderType string, terms int32) int32 {
	if orderType == "pos" {
		return 0
	}
	return terms
}


// --- rows -> views -----------------------------------------------------------

func orderFromRow(r db.GetSalesOrderByIDRow) *SalesOrder {
	// Derive payment status from amounts (not a DB column).
	paymentStatus := "unpaid"
	if r.PaidAmount.Equal(r.TotalAmount) && !r.TotalAmount.IsZero() {
		paymentStatus = "paid"
	} else if r.PaidAmount.GreaterThan(decimal.Zero) {
		paymentStatus = "partially_paid"
	}

	payMethod := r.PaymentMethod
	if payMethod == "" {
		payMethod = paymentMethodFromNotes(textToStrPtr(r.Notes))
	}
	if payMethod == "" && r.OrderType == "pos" {
		payMethod = "cash"
	}

	return &SalesOrder{
		ID:               r.ID,
		SoNumber:         r.SoNumber,
		CustomerID:       r.CustomerID,
		CustomerName:     textToStrPtr(r.CustomerName),
		SalesName:        textToStrPtr(r.SalesName),
		WarehouseName:    textToStrPtr(r.WarehouseName),
		WarehouseID:      int8ToPtr(r.WarehouseID),
		SalesID:          int8ToPtr(r.SalesID),
		StoreID:          int8ToPtr(r.StoreID),
		OrderDate:        r.OrderDate,
		Status:           r.Status,
		PaymentStatus:    paymentStatus,
		PaymentMethod:    payMethod,
		OrderType:        r.OrderType,
		// Normalize: POS (till) orders always display as cash regardless of
		// stored value (78 legacy rows carry payment_terms=7). Stored value is
		// immutable; display-only normalization.
		PaymentTerms:     effectivePaymentTerms(r.OrderType, r.PaymentTerms),
		SourceType:       textToStrPtr(r.SourceType),
		Subtotal:         r.Subtotal,
		TaxAmount:        r.TaxAmount,
		DiscountAmount:   r.DiscountAmount,
		TotalAmount:      r.TotalAmount,
		PaidAmount:       r.PaidAmount,
		RemainingAmount:  r.RemainingAmount,
		ShippingCost:     r.ShippingCost,
		ShippingAddress:  textToStrPtr(r.ShippingAddress),
		Notes:            textToStrPtr(r.Notes),
		ReceivedProofURL: textToStrPtr(r.ReceivedProofUrl),
		DeliveryDate:     dateToStrPtr(r.DeliveryDate),
		CreatedAt:        r.CreatedAt.Time,
		UpdatedAt:        r.UpdatedAt.Time,
	}
}

func orderFromListRow(r db.ListSalesOrdersRow) SalesOrder {
	// The list query carries three extra columns the detail query does not
	// (cashier_name / line_count / item_qty), so map the shared fields onto a
	// detail row explicitly rather than converting the struct wholesale.
	o := *orderFromRow(db.GetSalesOrderByIDRow{
		ID: r.ID, SoNumber: r.SoNumber, CustomerID: r.CustomerID, WarehouseID: r.WarehouseID,
		SalesID: r.SalesID, StoreID: r.StoreID, OrderDate: r.OrderDate, Status: r.Status,
		OrderType: r.OrderType, SourceType: r.SourceType, Subtotal: r.Subtotal, TaxAmount: r.TaxAmount,
		DiscountAmount: r.DiscountAmount, TotalAmount: r.TotalAmount, PaidAmount: r.PaidAmount,
		RemainingAmount: r.RemainingAmount, ShippingCost: r.ShippingCost, ShippingAddress: r.ShippingAddress,
		Notes: r.Notes, DeliveryDate: r.DeliveryDate, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt,
		CustomerName: r.CustomerName, SalesName: r.SalesName, WarehouseName: r.WarehouseName,
		PaymentTerms: r.PaymentTerms, PaymentMethod: r.PaymentMethod,
	})
	if r.CashierName != "" {
		name := r.CashierName
		o.CashierName = &name
	}
	o.LineCount = int(r.LineCount)
	q := r.ItemQty
	o.ItemQty = &q
	return o
}

func orderFromLockedRow(r db.GetSalesOrderByIDForUpdateRow) *SalesOrder {
	return orderFromRow(db.GetSalesOrderByIDRow(r))
}

func itemFromRow(r db.ListSalesOrderItemsRow) SOItem {
	return SOItem{
		ID:                       r.ID,
		SalesOrderID:             r.SalesOrderID,
		ProductID:                r.ProductID,
		Quantity:                 r.Quantity,
		UnitPrice:                r.UnitPrice,
		DiscountPercent:          r.DiscountPercent,
		DiscountAmount:           r.DiscountAmount,
		Subtotal:                 r.Subtotal,
		ProductUnitID:            int8ToPtr(r.ProductUnitID),
		UnitID:                   int8ToPtr(r.UnitID),
		UnitName:                 r.UnitName,
		UnitSymbol:               r.UnitSymbol,
		ConversionFactor:         r.ConversionFactor,
		BaseQuantity:             r.BaseQuantity,
		MinPurchase:              r.MinPurchase,
		ProductCode:              r.Code,
		ProductName:              r.Name,
		MembershipTier:           r.MembershipTier,
		MembershipDiscountAmount: r.MembershipDiscountAmount,
	}
}

// --- views -> params ---------------------------------------------------------

// sourceTypes enumerates the order source classification (business rules §sales):
// NOO = New Outlet Order, EC = placed as result of a Sales visit, AO = office order.
var sourceTypes = map[string]bool{"NOO": true, "EC": true, "AO": true}

// normalizeSourceType validates and canonicalizes an optional source_type
// input. nil stays nil (pos/ecommerce channels classify via order_type);
// empty string normalizes to nil; unknown values are rejected.
func normalizeSourceType(in *string) (*string, error) {
	if in == nil {
		return nil, nil
	}
	v := strings.ToUpper(strings.TrimSpace(*in))
	if v == "" {
		return nil, nil
	}
	if !sourceTypes[v] {
		return nil, fmt.Errorf("source_type must be one of NOO, EC, AO")
	}
	return &v, nil
}

func createOrderParams(o *SalesOrder) db.CreateSalesOrderParams {
	return db.CreateSalesOrderParams{
		SoNumber:        o.SoNumber,
		CustomerID:      o.CustomerID,
		WarehouseID:     int8OrNil(o.WarehouseID),
		SalesID:         int8OrNil(o.SalesID),
		OrderDate:       dateOrNil(o.OrderDate),
		Status:          o.Status,
		OrderType:       o.OrderType,
		Subtotal:        o.Subtotal,
		TaxAmount:       o.TaxAmount,
		DiscountAmount:  o.DiscountAmount,
		TotalAmount:     o.TotalAmount,
		PaidAmount:      o.PaidAmount,
		RemainingAmount: o.RemainingAmount,
		ShippingCost:    o.ShippingCost,
		ShippingAddress: textOrNilStr(o.ShippingAddress),
		Notes:           textOrNilStr(o.Notes),
		DeliveryDate:    dateOrNilStr(o.DeliveryDate),
		StoreID:         int8OrNil(o.StoreID),
		SourceType:      textOrNilStr(o.SourceType),
		PaymentTerms:    o.PaymentTerms,
	}
}

func updateOrderParams(o *SalesOrder) db.UpdateSalesOrderParams {
	return db.UpdateSalesOrderParams{
		ID:              o.ID,
		SoNumber:        o.SoNumber,
		CustomerID:      o.CustomerID,
		WarehouseID:     int8OrNil(o.WarehouseID),
		SalesID:         int8OrNil(o.SalesID),
		OrderDate:       dateOrNil(o.OrderDate),
		Status:          o.Status,
		OrderType:       o.OrderType,
		Subtotal:        o.Subtotal,
		TaxAmount:       o.TaxAmount,
		DiscountAmount:  o.DiscountAmount,
		TotalAmount:     o.TotalAmount,
		PaidAmount:      o.PaidAmount,
		RemainingAmount: o.RemainingAmount,
		ShippingCost:    o.ShippingCost,
		ShippingAddress: textOrNilStr(o.ShippingAddress),
		Notes:           textOrNilStr(o.Notes),
		DeliveryDate:    dateOrNilStr(o.DeliveryDate),
		StoreID:         int8OrNil(o.StoreID),
		SourceType:      textOrNilStr(o.SourceType),
		PaymentTerms:    o.PaymentTerms,
	}
}

func createItemParams(it *SOItem) db.CreateSalesOrderItemParams {
	return db.CreateSalesOrderItemParams{
		SalesOrderID:             it.SalesOrderID,
		ProductID:                it.ProductID,
		Quantity:                 it.Quantity,
		UnitPrice:                it.UnitPrice,
		DiscountPercent:          it.DiscountPercent,
		DiscountAmount:           it.DiscountAmount,
		Subtotal:                 it.Subtotal,
		ProductUnitID:            int8OrNil(it.ProductUnitID),
		UnitID:                   int8OrNil(it.UnitID),
		UnitName:                 it.UnitName,
		UnitSymbol:               it.UnitSymbol,
		ConversionFactor:         snapshotFactor(it.ConversionFactor),
		BaseQuantity:             it.baseQty(),
		MinPurchase:              snapshotFactor(it.MinPurchase),
		MembershipTier:           it.MembershipTier,
		MembershipDiscountAmount: it.MembershipDiscountAmount,
	}
}

// snapshotFactor keeps the DB CHECK (> 0) satisfied for callers that leave the
// conversion factor or minimum purchase unset.
func snapshotFactor(v decimal.Decimal) decimal.Decimal {
	if v.LessThanOrEqual(decimal.Zero) {
		return decimal.NewFromInt(1)
	}
	return v
}

func createInvoiceParams(inv *Invoice) db.CreateSalesInvoiceParams {
	return db.CreateSalesInvoiceParams{
		InvoiceNumber:   inv.InvoiceNumber,
		SalesOrderID:    int8(inv.SalesOrderID),
		CustomerID:      inv.CustomerID,
		InvoiceDate:     dateOrNil(inv.InvoiceDate),
		DueDate:         dateOrNil(inv.DueDate),
		Status:          inv.Status,
		Subtotal:        inv.Subtotal,
		TaxAmount:       inv.TaxAmount,
		TotalAmount:     inv.TotalAmount,
		PaidAmount:      inv.PaidAmount,
		RemainingAmount: inv.RemainingAmount,
	}
}

func createPaymentParams(p *Payment) db.CreateSalesPaymentParams {
	return db.CreateSalesPaymentParams{
		PaymentNumber: p.PaymentNumber,
		PayableType:   p.PayableType,
		PayableID:     p.PayableID,
		PaymentDate:   dateOrNil(p.PaymentDate),
		Amount:        p.Amount,
		PaymentMethod: p.PaymentMethod,
		Notes:         textOrNilStr(p.Notes),
		CreatedBy:     int8OrNil(p.CreatedBy),
	}
}

// --- pgtype helpers ----------------------------------------------------------

func int8ToPtr(v pgtype.Int8) *int64 {
	if !v.Valid {
		return nil
	}
	x := v.Int64
	return &x
}

func int8OrNil(v *int64) pgtype.Int8 {
	if v == nil {
		return pgtype.Int8{}
	}
	return pgtype.Int8{Int64: *v, Valid: true}
}

func int8(v int64) pgtype.Int8 {
	return pgtype.Int8{Int64: v, Valid: true}
}

func int8OrZero(v int64) pgtype.Int8 {
	if v <= 0 {
		return pgtype.Int8{}
	}
	return pgtype.Int8{Int64: v, Valid: true}
}

func textToStrPtr(v pgtype.Text) *string {
	if !v.Valid {
		return nil
	}
	s := v.String
	return &s
}

func textOrNilStr(v *string) pgtype.Text {
	if v == nil {
		return pgtype.Text{}
	}
	return pgtype.Text{String: *v, Valid: true}
}

func textOrNil(v string) pgtype.Text {
	if v == "" {
		return pgtype.Text{}
	}
	return pgtype.Text{String: v, Valid: true}
}

func dateOrNilStr(v *string) pgtype.Date {
	if v == nil {
		return pgtype.Date{}
	}
	return dateOrNil(*v)
}

func dateOrNil(v string) pgtype.Date {
	if v == "" {
		return pgtype.Date{}
	}
	if t, err := time.Parse("2006-01-02", v); err == nil {
		return pgtype.Date{Time: t, Valid: true}
	}
	return pgtype.Date{}
}

func dateToStrPtr(v pgtype.Date) *string {
	if !v.Valid {
		return nil
	}
	s := v.Time.Format("2006-01-02")
	return &s
}
