package service

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

	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
	expenseSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
	expenseValidation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
	kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
	projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
)

type PurchaseExpenseBridge interface {
	OnItemsDeleted(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error
	OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error
}

type ExpenseReceivingPayload struct {
	PurchaseItemID   uint
	ProductID        uint
	WarehouseID      uint
	SupplierID       uint
	TransportPerItem *float64
	ReceivedQty      float64
	ReceivedDate     *time.Time
	VehicleNumber    *string
}

type groupedItem struct {
	item       *entity.PurchaseItem
	payload    ExpenseReceivingPayload
	projectFK  *uint
	kandangID  *uint
	totalPrice float64
	poNumber   string
}

type expenseBridge struct {
	db                      *gorm.DB
	purchaseRepo            rPurchase.PurchaseRepository
	projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
	kandangRepo             kandangRepo.KandangRepository
	expenseSvc              expenseSvc.ExpenseService
}

func NewExpenseBridge(
	db *gorm.DB,
	purchaseRepo rPurchase.PurchaseRepository,
	projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
	kandangRepo kandangRepo.KandangRepository,
	expenseSvc expenseSvc.ExpenseService,
) PurchaseExpenseBridge {
	return &expenseBridge{
		db:                      db,
		purchaseRepo:            purchaseRepo,
		projectFlockKandangRepo: projectFlockKandangRepo,
		kandangRepo:             kandangRepo,
		expenseSvc:              expenseSvc,
	}
}

func (b *expenseBridge) OnItemsDeleted(ctx context.Context, _ uint, items []entity.PurchaseItem) error {
	if len(items) == 0 {
		return nil
	}

	return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		expenseIDs := make(map[uint64]struct{})
		expenseNonstockIDs := make([]uint64, 0)
		for _, item := range items {
			if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 {
				expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId)
			}
		}
		if len(expenseNonstockIDs) > 0 {
			for _, nsID := range expenseNonstockIDs {
				var expenseID uint64
				if err := tx.
					Model(&entity.ExpenseNonstock{}).
					Select("expense_id").
					Where("id = ?", nsID).
					Scan(&expenseID).Error; err != nil {
					return err
				}
				if expenseID != 0 {
					expenseIDs[expenseID] = struct{}{}
				}
			}
			if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
				return err
			}
		}

		var links []struct {
			ItemID            uint
			ExpenseNonstockID *uint64
		}
		if err := tx.
			Model(&entity.PurchaseItem{}).
			Select("id as item_id, expense_nonstock_id").
			Where("id IN ?", extractIDs(items)).
			Scan(&links).Error; err != nil {
			return err
		}

		for _, link := range links {
			if link.ExpenseNonstockID == nil || *link.ExpenseNonstockID == 0 {
				continue
			}
			var expenseID uint64
			if err := tx.
				Model(&entity.ExpenseNonstock{}).
				Select("expense_id").
				Where("id = ?", *link.ExpenseNonstockID).
				Scan(&expenseID).Error; err != nil {
				return err
			}
			if expenseID != 0 {
				expenseIDs[expenseID] = struct{}{}
			}
			if err := tx.Delete(&entity.ExpenseNonstock{}, *link.ExpenseNonstockID).Error; err != nil {
				return err
			}
		}

		approvalRepoTx := commonRepo.NewApprovalRepository(tx)
		for expenseID := range expenseIDs {
			var count int64
			if err := tx.Model(&entity.ExpenseNonstock{}).
				Where("expense_id = ?", expenseID).
				Count(&count).Error; err != nil {
				return err
			}
			if count == 0 {
				if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
					return err
				}
				if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
					return err
				}
			}
		}
		return nil
	})
}

func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error {
	if len(expenseIDs) == 0 {
		return nil
	}
	if actorID == 0 {
		actorID = 1
	}
	approvalRepo := commonRepo.NewApprovalRepository(b.db)
	svc := commonSvc.NewApprovalService(approvalRepo)
	action := entity.ApprovalActionCreated

	for id := range expenseIDs {
		latestApproval, err := approvalRepo.LatestByTarget(ctx, string(utils.ApprovalWorkflowExpense), uint(id), nil)
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}

		if latestApproval == nil {
			if _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
				return err
			}
		}
	}
	return nil
}

func (b *expenseBridge) clearExpenseLinksForItems(ctx context.Context, itemIDs []uint) error {
	if len(itemIDs) == 0 {
		return nil
	}

	unique := make(map[uint]struct{}, len(itemIDs))
	normalized := make([]uint, 0, len(itemIDs))
	for _, id := range itemIDs {
		if id == 0 {
			continue
		}
		if _, exists := unique[id]; exists {
			continue
		}
		unique[id] = struct{}{}
		normalized = append(normalized, id)
	}
	if len(normalized) == 0 {
		return nil
	}

	rows := make([]struct {
		ItemID            uint
		ExpenseNonstockID *uint64
		ExpenseID         *uint64
	}, 0, len(normalized))
	if err := b.db.WithContext(ctx).
		Table("purchase_items pi").
		Select("pi.id as item_id, pi.expense_nonstock_id, en.expense_id").
		Joins("LEFT JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id").
		Where("pi.id IN ?", normalized).
		Scan(&rows).Error; err != nil {
		return err
	}

	expenseNonstockIDs := make([]uint64, 0, len(rows))
	expenseIDs := make(map[uint64]struct{})
	for _, row := range rows {
		if row.ExpenseNonstockID != nil && *row.ExpenseNonstockID != 0 {
			expenseNonstockIDs = append(expenseNonstockIDs, *row.ExpenseNonstockID)
		}
		if row.ExpenseID != nil && *row.ExpenseID != 0 {
			expenseIDs[*row.ExpenseID] = struct{}{}
		}
	}

	return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Model(&entity.PurchaseItem{}).
			Where("id IN ?", normalized).
			Update("expense_nonstock_id", gorm.Expr("NULL")).Error; err != nil {
			return err
		}

		if len(expenseNonstockIDs) > 0 {
			if err := tx.Where("id IN ?", expenseNonstockIDs).Delete(&entity.ExpenseNonstock{}).Error; err != nil {
				return err
			}
		}

		approvalRepoTx := commonRepo.NewApprovalRepository(tx)
		for expenseID := range expenseIDs {
			var count int64
			if err := tx.Model(&entity.ExpenseNonstock{}).
				Where("expense_id = ?", expenseID).
				Count(&count).Error; err != nil {
				return err
			}
			if count > 0 {
				continue
			}

			if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
				return err
			}
			if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
				return err
			}
		}
		return nil
	})
}

func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error {
	if purchaseID == 0 || len(updates) == 0 {
		return nil
	}

	ctx := c.Context()
	clearExpenseLinks := make([]uint, 0, len(updates))
	filtered := make([]ExpenseReceivingPayload, 0, len(updates))
	for _, upd := range updates {
		if upd.PurchaseItemID == 0 {
			continue
		}
		if upd.SupplierID == 0 {
			clearExpenseLinks = append(clearExpenseLinks, upd.PurchaseItemID)
			continue
		}
		if upd.TransportPerItem == nil || *upd.TransportPerItem <= 0 {
			continue
		}
		if upd.VehicleNumber == nil || strings.TrimSpace(*upd.VehicleNumber) == "" {
			continue
		}
		filtered = append(filtered, upd)
	}
	if len(clearExpenseLinks) > 0 {
		if err := b.clearExpenseLinksForItems(ctx, clearExpenseLinks); err != nil {
			return err
		}
	}
	if len(filtered) == 0 {
		return nil
	}

	// Load current links to decide whether to update in place or recreate.
	type itemLink struct {
		ExpenseNonstockID uint64
		ExpenseID         uint64
		SupplierID        uint
		TransactionDate   time.Time
		Qty               float64
		Price             float64
	}

	purchase, err := b.purchaseRepo.GetByID(ctx, purchaseID, func(db *gorm.DB) *gorm.DB {
		return db.
			Preload("Items").
			Preload("Items.Product").
			Preload("Items.Warehouse").
			Preload("Items.Warehouse.Kandang")
	})
	if err != nil {
		return err
	}

	itemLinks := make(map[uint]itemLink)
	updatedExpenses := make(map[uint64]struct{})
	if len(filtered) > 0 {
		ids := make([]uint, 0, len(filtered))
		for _, upd := range filtered {
			if upd.PurchaseItemID != 0 {
				ids = append(ids, upd.PurchaseItemID)
			}
		}
		if len(ids) > 0 {
			rows := make([]struct {
				ItemID            uint
				ExpenseNonstockID uint64
				ExpenseID         uint64
				SupplierID        uint
				TransactionDate   time.Time
				Qty               float64
				Price             float64
			}, 0)
			if err := b.db.WithContext(ctx).
				Table("purchase_items pi").
				Select("pi.id as item_id, en.id as expense_nonstock_id, en.expense_id, e.supplier_id, e.transaction_date, en.qty, en.price").
				Joins("LEFT JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id").
				Joins("LEFT JOIN expenses e ON e.id = en.expense_id").
				Where("pi.id IN ?", ids).
				Scan(&rows).Error; err != nil {
				return err
			}
			// Build quick lookup per item and per group key for existing expenses.
			for _, row := range rows {
				itemLinks[row.ItemID] = itemLink{
					ExpenseNonstockID: row.ExpenseNonstockID,
					ExpenseID:         row.ExpenseID,
					SupplierID:        row.SupplierID,
					TransactionDate:   row.TransactionDate,
					Qty:               row.Qty,
					Price:             row.Price,
				}
			}
		}
	}

	itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items))
	for i := range purchase.Items {
		itemMap[purchase.Items[i].Id] = &purchase.Items[i]
	}

	groups := make(map[string][]groupedItem)

	for _, payload := range filtered {
		if payload.ReceivedDate == nil {
			return fiber.NewError(fiber.StatusBadRequest, "received_date is required")
		}
		item := itemMap[payload.PurchaseItemID]
		if item == nil {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
		}
		if payload.ReceivedQty <= 0 {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d must be greater than 0", payload.PurchaseItemID))
		}

		receivedDate := payload.ReceivedDate.UTC().Truncate(24 * time.Hour)
		supplierID := payload.SupplierID
		if supplierID == 0 {
			supplierID = purchase.SupplierId
		}

		// Decide whether to update existing expense_nonstock or create new.
		link, hasLink := itemLinks[payload.PurchaseItemID]
		if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
			oldDate := link.TransactionDate.UTC().Truncate(24 * time.Hour)
			newDate := receivedDate
			oldSupplier := link.SupplierID
			pricePerItem := item.Price
			if payload.TransportPerItem != nil {
				pricePerItem = *payload.TransportPerItem
			}

			// If supplier/date unchanged, update nonstock in place.
			if oldSupplier == supplierID && oldDate.Equal(newDate) {
				note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase))
				if err := b.db.WithContext(ctx).
					Model(&entity.ExpenseNonstock{}).
					Where("id = ?", link.ExpenseNonstockID).
					Updates(map[string]interface{}{
						"qty":   payload.ReceivedQty,
						"price": pricePerItem,
						"notes": note,
					}).Error; err != nil {
					return err
				}
				if link.ExpenseID != 0 {
					updatedExpenses[link.ExpenseID] = struct{}{}
				}
				continue
			}

			// Supplier/date changed: if the linked expense has only this nonstock, update it in place.
			if link.ExpenseID != 0 {
				var cnt int64
				if err := b.db.WithContext(ctx).
					Model(&entity.ExpenseNonstock{}).
					Where("expense_id = ?", link.ExpenseID).
					Count(&cnt).Error; err != nil {
					return err
				}
				if cnt == 1 {
					newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
					if err != nil {
						return err
					}
					note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase))
					if err := b.db.WithContext(ctx).
						Model(&entity.Expense{}).
						Where("id = ?", link.ExpenseID).
						Updates(map[string]interface{}{
							"transaction_date": newDate,
							"supplier_id":      supplierID,
						}).Error; err != nil {
						return err
					}
					updateBody := map[string]interface{}{
						"qty":         payload.ReceivedQty,
						"price":       pricePerItem,
						"notes":       note,
						"nonstock_id": newNonstockID,
					}
					if item.Warehouse != nil && item.Warehouse.KandangId != nil && *item.Warehouse.KandangId != 0 {
						updateBody["kandang_id"] = uint64(*item.Warehouse.KandangId)
					}
					if err := b.db.WithContext(ctx).
						Model(&entity.ExpenseNonstock{}).
						Where("id = ?", link.ExpenseNonstockID).
						Updates(updateBody).Error; err != nil {
						return err
					}
					updatedExpenses[link.ExpenseID] = struct{}{}
					continue
				}

				// Expense has multiple nonstocks: create new expense header for this item, then move existing nonstock to it.
				var kandangID *uint
				var projectFK *uint
				if item.Warehouse != nil && item.Warehouse.KandangId != nil {
					id := uint(*item.Warehouse.KandangId)
					kandangID = &id
					if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*item.Warehouse.KandangId)); err == nil && project != nil {
						pid := uint(project.Id)
						projectFK = &pid
					}
				}
				pricePerItem := item.Price
				if payload.TransportPerItem != nil {
					pricePerItem = *payload.TransportPerItem
				}
				totalPrice := pricePerItem * payload.ReceivedQty

				gItem := groupedItem{
					item:       item,
					payload:    payload,
					projectFK:  projectFK,
					kandangID:  kandangID,
					totalPrice: totalPrice,
					poNumber:   purchasePoNumber(purchase),
				}

				newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID)
				if err != nil {
					return err
				}

				expenseDetail, err := b.createExpenseViaService(c, purchase, []groupedItem{gItem}, newDate, newNonstockID, purchase.PoNumber, supplierID)
				if err != nil {
					return err
				}

				var createdNonstockID uint64
				if expenseDetail != nil {
					noteMap := mapExpenseNotes(expenseDetail)
					createdNonstockID = noteMap[payload.PurchaseItemID]
				}

				note := purchaseItemDisplayNote(item, payload.PurchaseItemID, purchasePoNumber(purchase))
				updateBody := map[string]interface{}{
					"expense_id":  expenseDetail.Id,
					"qty":         payload.ReceivedQty,
					"price":       pricePerItem,
					"notes":       note,
					"nonstock_id": newNonstockID,
				}
				if kandangID != nil {
					updateBody["kandang_id"] = uint64(*kandangID)
				} else {
					updateBody["kandang_id"] = nil
				}
				if projectFK != nil {
					updateBody["project_flock_kandang_id"] = uint64(*projectFK)
				} else {
					updateBody["project_flock_kandang_id"] = nil
				}

				if err := b.db.WithContext(ctx).
					Model(&entity.ExpenseNonstock{}).
					Where("id = ?", link.ExpenseNonstockID).
					Updates(updateBody).Error; err != nil {
					return err
				}

				if createdNonstockID != 0 {
					if err := b.db.WithContext(ctx).Delete(&entity.ExpenseNonstock{}, createdNonstockID).Error; err != nil {
						return err
					}
				}

				if link.ExpenseID != 0 {
					updatedExpenses[link.ExpenseID] = struct{}{}
				}
				if expenseDetail != nil && expenseDetail.Id != 0 {
					updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
				}
				continue
			}

			// Otherwise create new expense/nonstock in grouping flow.
		}

		baseKey := fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(receivedDate), payload.WarehouseID)
		key := baseKey
		if hasLink && link.ExpenseNonstockID != 0 && link.ExpenseID != 0 {
			key = fmt.Sprintf("%s:%d", baseKey, payload.PurchaseItemID)
		}

		var kandangID *uint
		var projectFK *uint
		if item.Warehouse != nil && item.Warehouse.KandangId != nil {
			id := uint(*item.Warehouse.KandangId)
			kandangID = &id
			if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*item.Warehouse.KandangId)); err == nil && project != nil {
				pid := uint(project.Id)
				projectFK = &pid
			}
		}

		pricePerItem := item.Price
		if payload.TransportPerItem != nil {
			pricePerItem = *payload.TransportPerItem
		}
		totalPrice := pricePerItem * payload.ReceivedQty

		groups[key] = append(groups[key], groupedItem{
			item:       item,
			payload:    payload,
			projectFK:  projectFK,
			kandangID:  kandangID,
			totalPrice: totalPrice,
			poNumber:   purchasePoNumber(purchase),
		})
	}

	for key, items := range groups {
		if len(items) == 0 {
			continue
		}
		parts := strings.Split(key, ":")
		if len(parts) < 3 {
			return errors.New("invalid expense grouping key")
		}
		expenseDate, err := utils.ParseDateString(parts[1])
		if err != nil {
			return err
		}

		supplierID, err := strconv.ParseUint(parts[0], 10, 64)
		if err != nil {
			return err
		}

		expeditionNonstockID, err := b.findExpeditionNonstockID(ctx, uint(supplierID))
		if err != nil {
			return err
		}

		expenseDetail, err := b.createExpenseViaService(c, purchase, items, expenseDate, expeditionNonstockID, purchase.PoNumber, uint(supplierID))
		if err != nil {
			return err
		}
		if err := b.linkExpenseNonstocksToItems(ctx, expenseDetail, items); err != nil {
			return err
		}
		if expenseDetail != nil && expenseDetail.Id != 0 {
			updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
		}
	}

	if len(updatedExpenses) > 0 {
		if err := b.markExpensesUpdated(ctx, updatedExpenses, purchase.CreatedBy); err != nil {
			return err
		}
	}

	return nil
}

func (b *expenseBridge) findExpeditionNonstockID(ctx context.Context, supplierID uint) (uint64, error) {
	var id uint64
	err := b.db.WithContext(ctx).
		Table("nonstocks AS ns").
		Select("ns.id").
		Joins("JOIN nonstock_suppliers nss ON nss.nonstock_id = ns.id").
		Joins("JOIN flags f ON f.flagable_id = ns.id AND f.flagable_type = ?", entity.FlagableTypeNonstock).
		Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))).
		Where("nss.supplier_id = ?", supplierID).
		Order("ns.id").
		Limit(1).
		Scan(&id).Error
	if err != nil {
		return 0, err
	}
	if id == 0 {
		return 0, fiber.NewError(fiber.StatusBadRequest, "supplier id tidak sesuai dengan expedisi")
	}
	return id, nil
}

func extractIDs(items []entity.PurchaseItem) []uint {
	result := make([]uint, 0, len(items))
	for _, item := range items {
		if item.Id != 0 {
			result = append(result, item.Id)
		}
	}
	return result
}

func (b *expenseBridge) createExpenseViaService(
	c *fiber.Ctx,
	purchase *entity.Purchase,
	items []groupedItem,
	expenseDate time.Time,
	expeditionNonstockID uint64,
	poNumber *string,
	supplierID uint,
) (*expenseDto.ExpenseDetailDTO, error) {
	ctx := c.Context()
	if b.expenseSvc == nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "expense service not available")
	}
	if len(items) == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "no items to create expense")
	}

	kandangID := items[0].kandangID
	var locationID uint64
	var expenseKandangID *uint64
	if kandangID != nil && *kandangID != 0 {
		kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB {
			return db.Select("id, location_id")
		})
		if err != nil {
			return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
		}
		if kandang == nil {
			return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID))
		}
		locationID = uint64(kandang.LocationId)
		id := uint64(*kandangID)
		expenseKandangID = &id
	} else {
		warehouse := items[0].item.Warehouse
		if warehouse == nil || warehouse.LocationId == nil || *warehouse.LocationId == 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse location is required for expense")
		}
		locationID = uint64(*warehouse.LocationId)
	}

	costItems := make([]expenseValidation.CostItem, 0, len(items))
	for _, gi := range items {
		note := fmt.Sprintf("purchase_item:%d", gi.payload.PurchaseItemID)
		price := gi.item.Price
		if gi.payload.TransportPerItem != nil {
			price = *gi.payload.TransportPerItem
		}
		costItems = append(costItems, expenseValidation.CostItem{
			NonstockID: expeditionNonstockID,
			Quantity:   gi.payload.ReceivedQty,
			Price:      price,
			Notes:      note,
		})
	}

	req := &expenseValidation.Create{
		PoNumber:        "",
		TransactionDate: utils.FormatDate(expenseDate),
		Category:        string(utils.ExpenseCategoryBOP),
		SupplierID:      uint64(supplierID),
		LocationID:      locationID,
		ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{
			KandangID: expenseKandangID,
			CostItems: costItems,
		}},
	}
	if poNumber != nil {
		req.PoNumber = *poNumber
	}

	detail, err := b.expenseSvc.CreateOne(c, req)
	if err != nil {
		return nil, err
	}

	// Mark approvals up to Finance so latest is Manager Finance
	action := entity.ApprovalActionApproved
	actorID := uint(purchase.CreatedBy)
	if actorID == 0 {
		actorID = 1
	}
	approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
	if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepHeadArea, &action, actorID, nil); err != nil {
		return nil, err
	}
	if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil {
		return nil, err
	}
	if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
		return nil, err
	}

	return detail, nil
}

func (b *expenseBridge) linkExpenseNonstocksToItems(ctx context.Context, detail *expenseDto.ExpenseDetailDTO, items []groupedItem) error {
	if detail == nil || len(items) == 0 {
		return nil
	}

	noteToExpenseNonstock := mapExpenseNotes(detail)

	if len(noteToExpenseNonstock) == 0 {
		return nil
	}

	for _, gi := range items {
		expenseNonstockID, ok := noteToExpenseNonstock[gi.payload.PurchaseItemID]
		if !ok {
			continue
		}
		if err := b.db.WithContext(ctx).
			Model(&entity.PurchaseItem{}).
			Where("id = ?", gi.payload.PurchaseItemID).
			Update("expense_nonstock_id", expenseNonstockID).Error; err != nil {
			return err
		}

		note := purchaseItemDisplayNote(gi.item, gi.payload.PurchaseItemID, gi.poNumber)
		if err := b.db.WithContext(ctx).
			Model(&entity.ExpenseNonstock{}).
			Where("id = ?", expenseNonstockID).
			Update("notes", note).Error; err != nil {
			return err
		}
	}

	return nil
}

func mapExpenseNotes(detail *expenseDto.ExpenseDetailDTO) map[uint]uint64 {
	result := make(map[uint]uint64)
	if detail == nil {
		return result
	}
	for _, kandang := range detail.Kandangs {
		for _, pengajuan := range kandang.Pengajuans {
			note := strings.TrimSpace(pengajuan.Notes)
			if note == "" {
				continue
			}
			const prefix = "purchase_item:"
			if !strings.HasPrefix(note, prefix) {
				continue
			}
			idStr := strings.TrimPrefix(note, prefix)
			var itemID uint
			if _, err := fmt.Sscanf(idStr, "%d", &itemID); err != nil {
				continue
			}
			result[itemID] = pengajuan.Id
		}
	}
	return result
}

func purchaseItemDisplayNote(item *entity.PurchaseItem, itemID uint, poNumber string) string {
	poLabel := "PO"
	if strings.TrimSpace(poNumber) != "" {
		poLabel = strings.TrimSpace(poNumber)
	}
	productName := fmt.Sprintf("Item %d", itemID)
	if item != nil && item.Product != nil && strings.TrimSpace(item.Product.Name) != "" {
		productName = item.Product.Name
	}
	return fmt.Sprintf("%s (%s)", poLabel, productName)
}

func purchasePoNumber(purchase *entity.Purchase) string {
	if purchase == nil || purchase.PoNumber == nil {
		return ""
	}
	return *purchase.PoNumber
}
