package inventory

import (
	"encoding/json"
	"time"

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

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

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

// stockLineFromRow builds the availability half of a stock row. The cost half
// is left nil here and attached by redactStockCosts, so the two decisions —
// "what is true" and "may this caller see it" — stay in separate functions and
// the row can never carry cost by accident on a path that forgot the check.
func stockLineFromRow(r db.ListStockLinesRow) StockLine {
	out := StockLine{
		ProductID:        r.ID,
		ProductCode:      r.Code,
		ProductName:      r.Name,
		CategoryName:     r.CategoryName,
		UnitSymbol:       textOr(r.UnitSymbol),
		Quantity:         r.Quantity,
		MinStock:         r.MinStock,
		IsActive:         r.IsActive,
		ReservedQuantity: r.ReservedQuantity,
		IsLowStock:       r.Quantity.LessThanOrEqual(r.MinStock),
	}
	if r.Outlets != nil {
		var b []byte
		switch v := r.Outlets.(type) {
		case []byte:
			b = v
		case string:
			b = []byte(v)
		default:
			b, _ = json.Marshal(r.Outlets)
		}
		_ = json.Unmarshal(b, &out.Outlets)
	}
	return out
}

func movementFromRow(r db.ListMovementsRow) Movement {
	return Movement{
		ID:             r.ID,
		MovementNumber: r.MovementNumber,
		ProductID:      r.ProductID,
		WarehouseID:    r.WarehouseID,
		MovementType:   r.MovementType,
		Quantity:       r.Quantity,
		QuantityBefore: r.QuantityBefore,
		QuantityAfter:  r.QuantityAfter,
		ReferenceType:  textToStrPtr(r.ReferenceType),
		ReferenceID:    int8ToPtr(r.ReferenceID),
		Notes:          textToStrPtr(r.Notes),
		CreatedBy:      int8ToPtr(r.CreatedBy),
		CreatedAt:      r.CreatedAt.Time,
		ProductName:    r.ProductName,
		ProductCode:    r.ProductCode,
		WarehouseName:  r.WarehouseName,
		Direction:      r.Direction,
	}
}

func opnameFromRow(r db.GetOpnameByIDRow) *Opname {
	return &Opname{
		ID:           r.ID,
		OpnameNumber: r.OpnameNumber,
		WarehouseID:  r.WarehouseID,
		OpnameDate:   r.OpnameDate,
		Status:       r.Status,
		Notes:        textToStrPtr(r.Notes),
		CreatedBy:    int8ToPtr(r.CreatedBy),
		ApprovedBy:   int8ToPtr(r.ApprovedBy),
	}
}

func opnameFromListRow(r db.ListOpnamesRow) Opname {
	return Opname{
		ID: r.ID, OpnameNumber: r.OpnameNumber, WarehouseID: r.WarehouseID,
		OpnameDate: r.OpnameDate, Status: r.Status,
		Notes: textToStrPtr(r.Notes), CreatedBy: int8ToPtr(r.CreatedBy), ApprovedBy: int8ToPtr(r.ApprovedBy),
		WarehouseName: r.WarehouseName, ItemCount: r.ItemCount,
	}
}

func opnameItemFromRow(r db.ListOpnameItemsRow) OpnameItem {
	it := OpnameItem{
		ID:             r.ID,
		StockOpnameID:  r.StockOpnameID,
		ProductID:      r.ProductID,
		SystemQuantity: r.SystemQuantity,
		PhysicalQty:    ndToDecPtr(r.PhysicalQuantity),
		Notes:          textToStrPtr(r.Notes),
		ProductCode:    r.Code,
		ProductName:    r.Name,
		UnitSymbol:     textOr(r.UnitSymbol),
	}
	// Difference exists only once a physical count exists; nil means "belum
	// dihitung", which the UI must render as empty, not as a variance.
	if it.PhysicalQty != nil {
		diff := it.PhysicalQty.Sub(it.SystemQuantity)
		it.Difference = &diff
	}
	return it
}

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

func createMovementParams(m *Movement) db.CreateMovementParams {
	return db.CreateMovementParams{
		MovementNumber: m.MovementNumber,
		ProductID:      m.ProductID,
		WarehouseID:    m.WarehouseID,
		MovementType:   m.MovementType,
		Quantity:       m.Quantity,
		QuantityBefore: m.QuantityBefore,
		QuantityAfter:  m.QuantityAfter,
		ReferenceType:  textOrNilStr(m.ReferenceType),
		ReferenceID:    int8OrNil(m.ReferenceID),
		Notes:          textOrNilStr(m.Notes),
		CreatedBy:      int8OrNil(m.CreatedBy),
	}
}

func createOpnameParams(o *Opname) db.CreateOpnameParams {
	return db.CreateOpnameParams{
		OpnameNumber: o.OpnameNumber,
		WarehouseID:  o.WarehouseID,
		OpnameDate:   dateOrNil(o.OpnameDate),
		Status:       o.Status,
		Notes:        textOrNilStr(o.Notes),
		CreatedBy:    int8OrNil(o.CreatedBy),
	}
}

func createOpnameItemParams(it *OpnameItem) db.CreateOpnameItemParams {
	return db.CreateOpnameItemParams{
		StockOpnameID:    it.StockOpnameID,
		ProductID:        it.ProductID,
		SystemQuantity:   it.SystemQuantity,
		PhysicalQuantity: decToNdPtr(it.PhysicalQty),
		Notes:            textOrNilStr(it.Notes),
	}
}

// --- 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 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 textOr(v pgtype.Text) string {
	if !v.Valid {
		return ""
	}
	return v.String
}

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

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
}

func ndToDecPtr(v decimal.NullDecimal) *decimal.Decimal {
	if !v.Valid {
		return nil
	}
	d := v.Decimal
	return &d
}

func decToNdPtr(v *decimal.Decimal) decimal.NullDecimal {
	if v == nil {
		return decimal.NullDecimal{}
	}
	return decimal.NullDecimal{Decimal: *v, Valid: true}
}
