package purchasing

import (
	"context"
	"errors"
	"fmt"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"

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

// Repository implements Store on a pgx pool.
type Repository struct {
	pool *pgxpool.Pool
	q    *db.Queries
}

func NewRepository(pool *pgxpool.Pool) *Repository {
	return &Repository{pool: pool, q: db.New(pool)}
}

func (r *Repository) Begin(ctx context.Context) (Tx, error) { return r.pool.Begin(ctx) }

func (r *Repository) SupplierExists(ctx context.Context, id int64) (bool, error) {
	n, err := r.q.CountSuppliersActive(ctx, id)
	return n > 0, err
}

func (r *Repository) WarehouseExists(ctx context.Context, id int64) (bool, error) {
	n, err := r.q.CountWarehousesActive(ctx, id)
	return n > 0, err
}

func (r *Repository) ProductExists(ctx context.Context, id int64) (bool, error) {
	n, err := r.q.CountProductsActive(ctx, id)
	return n > 0, err
}

func (r *Repository) CreateOrder(ctx context.Context, q db.DBTX, p db.CreatePurchaseOrderParams) (db.CreatePurchaseOrderRow, error) {
	return db.New(q).CreatePurchaseOrder(ctx, p)
}

func (r *Repository) UpdateOrder(ctx context.Context, q db.DBTX, p db.UpdatePurchaseOrderParams) error {
	n, err := db.New(q).UpdatePurchaseOrder(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) DeleteOrder(ctx context.Context, q db.DBTX, id int64) error {
	n, err := db.New(q).DeletePurchaseOrder(ctx, id)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) OrderByID(ctx context.Context, id int64) (db.GetPurchaseOrderByIDRow, error) {
	row, err := r.q.GetPurchaseOrderByID(ctx, id)
	if errors.Is(err, pgx.ErrNoRows) {
		return db.GetPurchaseOrderByIDRow{}, ErrNotFound
	}
	return row, err
}

func (r *Repository) OrderByIDForUpdate(ctx context.Context, q db.DBTX, id int64) (db.GetPurchaseOrderByIDForUpdateRow, error) {
	row, err := db.New(q).GetPurchaseOrderByIDForUpdate(ctx, id)
	if errors.Is(err, pgx.ErrNoRows) {
		return db.GetPurchaseOrderByIDForUpdateRow{}, ErrNotFound
	}
	return row, err
}

func (r *Repository) ListOrders(ctx context.Context, f POFilter) ([]db.ListPurchaseOrdersRow, int, error) {
	filter := db.CountPurchaseOrdersParams{Search: textOrNil(f.Search), Status: textOrNil(f.Status), SupplierID: positiveInt8(f.SupplierID)}
	total, err := r.q.CountPurchaseOrders(ctx, filter)
	if err != nil {
		return nil, 0, err
	}
	rows, err := r.q.ListPurchaseOrders(ctx, db.ListPurchaseOrdersParams{
		Limit: int32(f.PerPage), Offset: int32((f.Page - 1) * f.PerPage),
		Search: filter.Search, Status: filter.Status, SupplierID: filter.SupplierID,
		SortBy:  sortByOrNil(f.SortBy, poSortColumns),
		SortDir: sortDirOrNil(f.SortDir),
	})
	return rows, int(total), err
}

func (r *Repository) CreateItem(ctx context.Context, q db.DBTX, p db.CreatePurchaseOrderItemParams) (int64, error) {
	return db.New(q).CreatePurchaseOrderItem(ctx, p)
}

func (r *Repository) ItemsByOrder(ctx context.Context, orderID int64) ([]db.ListPurchaseOrderItemsRow, error) {
	return r.q.ListPurchaseOrderItems(ctx, orderID)
}

func (r *Repository) ItemsByOrderForUpdate(ctx context.Context, q db.DBTX, orderID int64) ([]db.ListPurchaseOrderItemsForUpdateRow, error) {
	return db.New(q).ListPurchaseOrderItemsForUpdate(ctx, orderID)
}

func (r *Repository) DeleteItems(ctx context.Context, q db.DBTX, orderID int64) error {
	return db.New(q).DeletePurchaseOrderItems(ctx, orderID)
}

func (r *Repository) SetOrderStatus(ctx context.Context, q db.DBTX, p db.SetPurchaseOrderStatusParams) error {
	n, err := db.New(q).SetPurchaseOrderStatus(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) ApproveOrder(ctx context.Context, q db.DBTX, p db.ApprovePurchaseOrderParams) error {
	n, err := db.New(q).ApprovePurchaseOrder(ctx, p)
	if err != nil {
		return err
	}
	// The service locks the draft row first, so zero means a concurrent
	// state change slipped between lock and write — never a silent approve.
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

// StampPrint runs the print-once ledger UPDATE (000150). The service reads
// execrows: 0 means "already printed and the caller may not reprint" — the
// atomic WHERE makes two concurrent first prints pick exactly one winner.
func (r *Repository) StampPrint(ctx context.Context, p db.PurchaseOrdersStampPrintParams) (int64, error) {
	return db.New(r.pool).PurchaseOrdersStampPrint(ctx, p)
}

func (r *Repository) AddReceived(ctx context.Context, q db.DBTX, p db.AddReceivedQuantityParams) error {
	n, err := db.New(q).AddReceivedQuantity(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) CreateGR(ctx context.Context, q db.DBTX, p db.CreateGoodsReceiptParams) (db.CreateGoodsReceiptRow, error) {
	return db.New(q).CreateGoodsReceipt(ctx, p)
}

func (r *Repository) CreateGRItem(ctx context.Context, q db.DBTX, p db.CreateGoodsReceiptItemParams) (int64, error) {
	return db.New(q).CreateGoodsReceiptItem(ctx, p)
}

func (r *Repository) GRByID(ctx context.Context, id int64) (db.GetGoodsReceiptByIDRow, error) {
	row, err := r.q.GetGoodsReceiptByID(ctx, id)
	if errors.Is(err, pgx.ErrNoRows) {
		return db.GetGoodsReceiptByIDRow{}, ErrNotFound
	}
	return row, err
}

func (r *Repository) GRItems(ctx context.Context, grID int64) ([]db.ListGoodsReceiptItemsRow, error) {
	return r.q.ListGoodsReceiptItems(ctx, grID)
}

func (r *Repository) ListGRs(ctx context.Context, f GRFilter) ([]db.ListGoodsReceiptsRow, int, error) {
	status := textOrNil(f.Status)
	total, err := r.q.CountGoodsReceipts(ctx, status)
	if err != nil {
		return nil, 0, err
	}
	rows, err := r.q.ListGoodsReceipts(ctx, db.ListGoodsReceiptsParams{
		Limit: int32(f.PerPage), Offset: int32((f.Page - 1) * f.PerPage), Status: status,
	})
	if err != nil {
		return nil, 0, err
	}
	return rows, int(total), nil
}

func (r *Repository) ReceiveStock(ctx context.Context, q db.DBTX, p db.RawStockAddParams, grID, itemID int64, userID *int64) error {
	if err := db.New(q).EnsureStockRow(ctx, db.EnsureStockRowParams{ProductID: p.ProductID, WarehouseID: p.WarehouseID}); err != nil {
		return err
	}
	stock, err := db.New(q).GetStockRowForUpdate(ctx, db.GetStockRowForUpdateParams{ProductID: p.ProductID, WarehouseID: p.WarehouseID})
	if err != nil {
		return err
	}
	after := stock.Quantity.Add(p.Quantity)
	_, err = db.New(q).CreateMovement(ctx, db.CreateMovementParams{
		MovementNumber: fmt.Sprintf("SM-GR-%d-%d", grID, itemID), ProductID: p.ProductID, WarehouseID: p.WarehouseID,
		MovementType: "stock_in", Quantity: p.Quantity, QuantityBefore: stock.Quantity, QuantityAfter: after,
		ReferenceType: pgtype.Text{String: "goods_receipt", Valid: true}, ReferenceID: pgtype.Int8{Int64: grID, Valid: true}, CreatedBy: int8OrNil(userID),
	})
	if err != nil {
		return err
	}
	count, err := db.New(q).UpdateStockRow(ctx, db.UpdateStockRowParams{ID: stock.ID, Quantity: after, ReservedQuantity: stock.ReservedQuantity})
	if err != nil {
		return err
	}
	if count == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) ListDebts(ctx context.Context, f DebtFilter) ([]db.ListDebtsRow, int, DebtSummary, error) {
	filter := db.SumDebtsParams{SupplierID: positiveInt8(f.SupplierID), DueDate: dateOrNil(f.DueDate), Search: textOrNil(f.Search)}
	stats, err := r.q.SumDebts(ctx, filter)
	if err != nil {
		return nil, 0, DebtSummary{}, err
	}
	countFilter := db.CountDebtsParams{
		SupplierID: filter.SupplierID,
		DueDate:    filter.DueDate,
		Search:     filter.Search,
		DebtStatus: textOrNil(f.DebtStatus),
	}
	count, err := r.q.CountDebts(ctx, countFilter)
	if err != nil {
		return nil, 0, DebtSummary{}, err
	}
	rows, err := r.q.ListDebts(ctx, db.ListDebtsParams{
		Limit: int32(f.PerPage), Offset: int32((f.Page - 1) * f.PerPage),
		SortBy: sortByOrNil(f.SortBy, debtSortColumns), SortDir: sortDirOrNil(f.SortDir),
		SupplierID: filter.SupplierID, DueDate: filter.DueDate, Search: filter.Search,
		DebtStatus: textOrNil(f.DebtStatus),
	})
	summary := DebtSummary{
		TotalAmount:     stats.Total,
		TotalPaid:       stats.TotalPaid,
		TotalPO:         stats.TotalPo,
		CountUnpaid:     stats.CountUnpaid,
		ActiveSuppliers: stats.SupplierUnpaidCount,
	}
	return rows, int(count), summary, err
}

func (r *Repository) CreateVoucher(ctx context.Context, q db.DBTX, p db.CreateVoucherParams) (int64, error) {
	return db.New(q).CreateVoucher(ctx, p)
}

func (r *Repository) VoucherByID(ctx context.Context, id int64) (db.GetVoucherByIDRow, error) {
	row, err := r.q.GetVoucherByID(ctx, id)
	if errors.Is(err, pgx.ErrNoRows) {
		return db.GetVoucherByIDRow{}, ErrNotFound
	}
	return row, err
}

func (r *Repository) VoucherByIDForUpdate(ctx context.Context, q db.DBTX, id int64) (db.GetVoucherByIDForUpdateRow, error) {
	row, err := db.New(q).GetVoucherByIDForUpdate(ctx, id)
	if errors.Is(err, pgx.ErrNoRows) {
		return db.GetVoucherByIDForUpdateRow{}, ErrNotFound
	}
	return row, err
}

func (r *Repository) UpdateVoucherStatus(ctx context.Context, q db.DBTX, p db.UpdateVoucherStatusParams) error {
	n, err := db.New(q).UpdateVoucherStatus(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrVoucherUsed
	}
	return nil
}

func (r *Repository) UpdateVoucherStatusAny(ctx context.Context, q db.DBTX, p db.UpdateVoucherStatusAnyParams) error {
	n, err := db.New(q).UpdateVoucherStatusAny(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) UpdateVoucher(ctx context.Context, q db.DBTX, p db.UpdateVoucherParams) error {
	n, err := db.New(q).UpdateVoucher(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) ListVouchers(ctx context.Context, f VoucherFilter) ([]db.ListVouchersRow, int, error) {
	status := textOrNil(f.Status)
	total, err := r.q.CountVouchers(ctx, status)
	if err != nil {
		return nil, 0, err
	}
	rows, err := r.q.ListVouchers(ctx, db.ListVouchersParams{
		Limit: int32(f.PerPage), Offset: int32((f.Page - 1) * f.PerPage), Status: status,
	})
	if err != nil {
		return nil, 0, err
	}
	return rows, int(total), nil
}

func (r *Repository) MarkVoucherPrinted(ctx context.Context, q db.DBTX, p db.MarkVoucherPrintedParams) error {
	n, err := db.New(q).MarkVoucherPrinted(ctx, p)
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

func (r *Repository) PurchaseOrderSupplierID(ctx context.Context, q db.DBTX, poID int64) (int64, error) {
	id, err := db.New(q).GetPurchaseOrderSupplierID(ctx, poID)
	if errors.Is(err, pgx.ErrNoRows) {
		return 0, ErrNotFound
	}
	return id, err
}

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