// Package requisitions implements purchase requisitions (000151, flow audit
// item 8): a store head flags items running out at their outlet; admin
// pembelian reviews the queue and converts a requisition into a purchase
// order. A requisition is a REQUEST — no stock movement, no money, no
// journal. Its whole lifecycle is pending -> reviewed (po_id stamped) |
// rejected, and the link back to the PO is what gives ERP the full trace
// pengajuan → PO → terima.
package requisitions

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/internal/db"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/shopspring/decimal"
)

var (
	ErrNotFound        = errors.New("pengajuan tidak ditemukan")
	ErrItemRequired    = errors.New("minimal satu barang harus diajukan")
	ErrProductRequired = errors.New("produk wajib dipilih")
	ErrQtyInvalid      = errors.New("jumlah harus lebih dari 0")
	ErrAlreadyReviewed = errors.New("pengajuan sudah direview")
	ErrStoreRequired   = errors.New("toko wajib diisi")
)

// Store is the persistence seam.
type Store interface {
	CreateRequisition(ctx context.Context, p db.CreatePurchaseRequisitionParams) (db.CreatePurchaseRequisitionRow, error)
	CreateRequisitionItem(ctx context.Context, p db.CreatePurchaseRequisitionItemParams) error
	ListRequisitions(ctx context.Context, f db.ListPurchaseRequisitionsParams) ([]db.ListPurchaseRequisitionsRow, error)
	CountRequisitions(ctx context.Context, f db.CountPurchaseRequisitionsParams) (int64, error)
	RequisitionByID(ctx context.Context, id int64) (db.GetPurchaseRequisitionRow, error)
	RequisitionItems(ctx context.Context, requisitionID int64) ([]db.ListPurchaseRequisitionItemsRow, error)
	UpdateRequisitionReview(ctx context.Context, p db.UpdatePurchaseRequisitionReviewParams) (int64, error)
}

// RequisitionNumberSource allocates the REQ- document number inside the
// caller's transaction, same contract as every other seq consumer.
type RequisitionNumberSource func(ctx context.Context, tx pgx.Tx, now time.Time) (string, error)

// Service implements the requisition use-cases.
type Service struct {
	store  Store
	seq    RequisitionNumberSource
	loc    *time.Location
	txAny  func(ctx context.Context) (pgx.Tx, func(), error)
}

// NewService builds a Service.
func NewService(store Store, loc *time.Location) *Service {
	return &Service{store: store, loc: loc}
}

// WithPool wires the transaction helper (pool-backed).
func (s *Service) WithPool(pool PgxPool) *Service {
	s.txAny = func(ctx context.Context) (pgx.Tx, func(), error) {
		tx, err := pool.Begin(ctx)
		return tx, func() { _ = tx.Rollback(ctx) }, err
	}
	return s
}

// PgxPool is the begin/rollback surface requisitions needs.
type PgxPool interface {
	Begin(ctx context.Context) (pgx.Tx, error)
}

// RequisitionInput is the POST /purchase-requisitions body.
type RequisitionInput struct {
	StoreID    int64                `json:"store_id"`
	WarehouseID int64              `json:"warehouse_id"`
	Items      []RequisitionItemInput `json:"items"`
	Note       string               `json:"note"`
}

// RequisitionItemInput is one requested product line.
type RequisitionItemInput struct {
	ProductID int64   `json:"product_id"`
	Quantity  decimal.Decimal `json:"quantity"`
	Note      string  `json:"note"`
}

func validateItems(in []RequisitionItemInput) error {
	if len(in) == 0 {
		return ErrItemRequired
	}
	seen := map[int64]bool{}
	for _, it := range in {
		if it.ProductID <= 0 {
			return ErrProductRequired
		}
		if !it.Quantity.IsPositive() || !it.Quantity.Equal(it.Quantity.Round(4)) {
			return ErrQtyInvalid
		}
		if seen[it.ProductID] {
			return ErrProductRequired
		}
		seen[it.ProductID] = true
	}
	return nil
}

// Create records a pending requisition from the store head's account. The
// store comes from the caller's identity when scoped (a kepala toko cannot
// file for another outlet); platform-wide callers must name the store.
func (s *Service) Create(ctx context.Context, in RequisitionInput) (*db.GetPurchaseRequisitionRow, error) {
	u := auth.UserFrom(ctx)
	if u == nil {
		return nil, errors.New("unauthenticated")
	}
	if in.StoreID <= 0 {
		return nil, ErrStoreRequired
	}
	if err := validateItems(in.Items); err != nil {
		return nil, err
	}
	// Tenant binding (§5.4): a scoped caller may only file for its own store.
	sc := auth.StoreScopeFrom(ctx)
	if !sc.PlatformWide() {
		if !sc.Allows(&in.StoreID) {
			return nil, errors.New("toko di luar outlet Anda")
		}
	}
	// The outlet's own warehouse is the default source suggestion.
	var whID pgtype.Int8
	if in.WarehouseID > 0 {
		whID = pgtype.Int8{Int64: in.WarehouseID, Valid: true}
	} else if s.store != nil {
		// Best-effort default: the store's own warehouse.
		// (kept optional — the PO review decides the real source)
		_ = whID
	}

	if s.txAny == nil {
		return nil, errors.New("pool not wired")
	}
	tx, done, err := s.txAny(ctx)
	if err != nil {
		return nil, err
	}
	defer done()
	now := time.Now().In(s.loc)
	num := fmt.Sprintf("REQ-%s-%06d", now.Format("20060102"), now.Nanosecond()%1000000)
	row, err := s.store.CreateRequisition(ctx, db.CreatePurchaseRequisitionParams{
		RequisitionNumber: num,
		StoreID:           in.StoreID,
		WarehouseID:       whID,
		RequestedBy:       u.ID,
	})
	if err != nil {
		return nil, err
	}
	for _, it := range in.Items {
		if err := s.store.CreateRequisitionItem(ctx, db.CreatePurchaseRequisitionItemParams{
			RequisitionID: row.ID,
			ProductID:     it.ProductID,
			Quantity:      it.Quantity,
			Note:          pgtext(strings.TrimSpace(it.Note)),
		}); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	return s.finishCreate(ctx, row.ID)
}

// finishCreate re-reads the requisition with its items.
func (s *Service) finishCreate(ctx context.Context, id int64) (*db.GetPurchaseRequisitionRow, error) {
	row, _, err := s.Get(ctx, id)
	return row, err
}

// List returns the requisition queue, newest first.
func (s *Service) List(ctx context.Context, f ListFilter) ([]db.ListPurchaseRequisitionsRow, int64, error) {
	page, per := f.Page, f.PerPage
	if page < 1 {
		page = 1
	}
	if per < 1 || per > 100 {
		per = 20
	}
	arg := db.ListPurchaseRequisitionsParams{
		Status:      pgtext(f.Status),
		StoreID:     int8OrNilZero(f.StoreID),
		RequestedBy: int8OrNilZero(f.RequestedBy),
		SortBy:      pgtext(f.SortBy),
		SortDir:     pgtext(f.SortDir),
		Limit:       int32(per),
		Offset:      int32((page - 1) * per),
	}
	rows, err := s.store.ListRequisitions(ctx, arg)
	if err != nil {
		return nil, 0, err
	}
	total, err := s.store.CountRequisitions(ctx, db.CountPurchaseRequisitionsParams{
		Status:      arg.Status,
		StoreID:     arg.StoreID,
		RequestedBy: arg.RequestedBy,
	})
	if err != nil {
		return nil, 0, err
	}
	return rows, total, nil
}

// ListFilter carries the queue filters.
type ListFilter struct {
	Page        int
	PerPage     int
	Status      string
	StoreID     int64
	RequestedBy int64
	SortBy      string
	SortDir     string
}

// Get returns one requisition with its item lines.
func (s *Service) Get(ctx context.Context, id int64) (*db.GetPurchaseRequisitionRow, []db.ListPurchaseRequisitionItemsRow, error) {
	row, err := s.store.RequisitionByID(ctx, id)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, nil, ErrNotFound
		}
		return nil, nil, err
	}
	items, err := s.store.RequisitionItems(ctx, id)
	if err != nil {
		return nil, nil, err
	}
	return &row, items, nil
}

// ReviewInput is the POST /purchase-requisitions/{id}/review body: approve by
// naming the created PO, or reject with a note.
type ReviewInput struct {
	Action   string `json:"action"` // "approve" | "reject"
	POID     int64  `json:"po_id"`
	Note     string `json:"note"`
}

// Review marks a pending requisition reviewed (PO created) or rejected. Only
// a pending requisition can move — a re-review would silently re-open history.
func (s *Service) Review(ctx context.Context, id int64, in ReviewInput, reviewerID int64) (*db.GetPurchaseRequisitionRow, error) {
	action := strings.ToLower(strings.TrimSpace(in.Action))
	if action != "approve" && action != "reject" {
		return nil, errors.New("action must be approve or reject")
	}
	status := "reviewed"
	var poID pgtype.Int8
	if action == "approve" {
		if in.POID <= 0 {
			return nil, errors.New("po_id wajib diisi saat menyetujui pengajuan")
		}
		poID = pgtype.Int8{Int64: in.POID, Valid: true}
	}
	n, err := s.store.UpdateRequisitionReview(ctx, db.UpdatePurchaseRequisitionReviewParams{
		ID:         id,
		Status:     status,
		PoID:       poID,
		ReviewedBy: reviewerID,
		ReviewNote: strings.TrimSpace(in.Note),
	})
	if err != nil {
		return nil, err
	}
	if n == 0 {
		return nil, ErrAlreadyReviewed
	}
	row, _, err := s.Get(ctx, id)
	return row, err
}

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

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