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"
	rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
	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"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
)

type TransferExpenseBridge interface {
	OnItemsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error
	OnItemsDelivered(c *fiber.Ctx, transferID uint64, updates []TransferExpenseReceivingPayload) error
}

type TransferExpenseReceivingPayload struct {
	TransferDetailID uint64
	ProductID        uint64
	WarehouseID      uint64
	SupplierID       uint64
	TransportPerItem *float64
	DeliveredQty     float64
	DeliveredDate    *time.Time
}

type groupedTransferItem struct {
	detail            *entity.StockTransferDetail
	payload           TransferExpenseReceivingPayload
	projectFK         *uint
	kandangID         *uint
	totalPrice        float64
	shippingCostTotal float64
}

func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
	return fmt.Sprintf("%d:%s:%d", supplierID, utils.FormatDate(date), warehouseID)
}

type transferExpenseBridge struct {
	db                      *gorm.DB
	transferRepo            rStockTransfer.StockTransferRepository
	projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
	kandangRepo             kandangRepo.KandangRepository
	expenseSvc              expenseSvc.ExpenseService
}

func NewTransferExpenseBridge(
	db *gorm.DB,
	transferRepo rStockTransfer.StockTransferRepository,
	projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
	kandangRepo kandangRepo.KandangRepository,
	expenseSvc expenseSvc.ExpenseService,
) TransferExpenseBridge {
	return &transferExpenseBridge{
		db:                      db,
		transferRepo:            transferRepo,
		projectFlockKandangRepo: projectFlockKandangRepo,
		kandangRepo:             kandangRepo,
		expenseSvc:              expenseSvc,
	}
}

func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, items []entity.StockTransferDetail) 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
			}
		}

		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 *transferExpenseBridge) 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 *transferExpenseBridge) 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 (b *transferExpenseBridge) createExpenseViaService(
	c *fiber.Ctx,
	transfer *entity.StockTransfer,
	items []groupedTransferItem,
	expenseDate time.Time,
	expeditionNonstockID uint64,
	movementNumber 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 {

		if transfer.ToWarehouse == nil || transfer.ToWarehouse.LocationId == nil || *transfer.ToWarehouse.LocationId == 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, "Destination warehouse location is required for expense")
		}
		locationID = uint64(*transfer.ToWarehouse.LocationId)
	}

	costItems := make([]expenseValidation.CostItem, 0, len(items))
	for _, gi := range items {
		note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id)

		price := gi.shippingCostTotal
		if gi.payload.TransportPerItem != nil {
			price = *gi.payload.TransportPerItem * gi.payload.DeliveredQty
		}

		costItems = append(costItems, expenseValidation.CostItem{
			NonstockID: expeditionNonstockID,
			Quantity:   1,
			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,
		}},
	}

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

	action := entity.ApprovalActionApproved
	actorID := uint(transfer.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 *transferExpenseBridge) linkExpenseNonstocksToDetails(ctx context.Context, detail *expenseDto.ExpenseDetailDTO, items []groupedTransferItem) error {
	if detail == nil || len(items) == 0 {
		return nil
	}

	noteToExpenseNonstock := mapExpenseNotesForTransfer(detail)

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

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

	return nil
}

func mapExpenseNotesForTransfer(detail *expenseDto.ExpenseDetailDTO) map[uint64]uint64 {
	result := make(map[uint64]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 = "stock_transfer_detail:"
			if !strings.HasPrefix(note, prefix) {
				continue
			}
			idStr := strings.TrimPrefix(note, prefix)
			var detailID uint64
			if _, err := fmt.Sscanf(idStr, "%d", &detailID); err != nil {
				continue
			}
			result[detailID] = pengajuan.Id
		}
	}
	return result
}

func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64, updates []TransferExpenseReceivingPayload) error {
	if transferID == 0 || len(updates) == 0 {
		return nil
	}

	ctx := c.Context()

	transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB {
		return db.
			Preload("Details").
			Preload("Details.Product").
			Preload("Details.DestProductWarehouse").
			Preload("Details.DeliveryItems").
			Preload("Details.DeliveryItems.StockTransferDelivery").
			Preload("ToWarehouse")
	})
	if err != nil {
		return err
	}

	detailMap := make(map[uint64]*entity.StockTransferDetail, len(transfer.Details))
	shippingCostMap := make(map[uint64]float64) // detailID -> ShippingCostTotal

	for i := range transfer.Details {
		detailMap[transfer.Details[i].Id] = &transfer.Details[i]

		for _, deliveryItem := range transfer.Details[i].DeliveryItems {
			if deliveryItem.StockTransferDelivery != nil {
				shippingCostMap[transfer.Details[i].Id] = deliveryItem.StockTransferDelivery.ShippingCostTotal
				break
			}
		}
	}

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

	for _, payload := range updates {
		if payload.DeliveredDate == nil {
			return fiber.NewError(fiber.StatusBadRequest, "delivered_date is required")
		}
		detail := detailMap[payload.TransferDetailID]
		if detail == nil {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer detail %d not found", payload.TransferDetailID))
		}
		if payload.DeliveredQty <= 0 {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Delivered quantity for detail %d must be greater than 0", payload.TransferDetailID))
		}

		deliveredDate := payload.DeliveredDate.UTC().Truncate(24 * time.Hour)
		supplierID := payload.SupplierID
		if supplierID == 0 {
			supplierID = 1 // Default supplier
		}

		var kandangID *uint
		var projectFK *uint
		if detail.DestProductWarehouse.WarehouseId != 0 {
			var kandangIDResult uint
			if err := b.db.WithContext(ctx).
				Table("warehouses").
				Select("kandang_id").
				Where("id = ?", detail.DestProductWarehouse.WarehouseId).
				Scan(&kandangIDResult).Error; err == nil && kandangIDResult != 0 {
				id := uint(kandangIDResult)
				kandangID = &id
				if project, err := b.projectFlockKandangRepo.GetActiveByKandangID(ctx, kandangIDResult); err == nil && project != nil {
					pid := uint(project.Id)
					projectFK = &pid
				}
			}
		}

		shippingCostTotal := shippingCostMap[detail.Id]

		totalPrice := shippingCostTotal
		if payload.TransportPerItem != nil {

			totalPrice = *payload.TransportPerItem * payload.DeliveredQty
		}

		warehouseID := uint(payload.WarehouseID)
		if warehouseID == 0 && transfer.ToWarehouse != nil {
			warehouseID = uint(transfer.ToWarehouse.Id)
		}
		if warehouseID == 0 && detail.DestProductWarehouse != nil {
			warehouseID = uint(detail.DestProductWarehouse.WarehouseId)
		}

		key := groupingKey(uint(supplierID), deliveredDate, warehouseID)
		groups[key] = append(groups[key], groupedTransferItem{
			detail:            detail,
			payload:           payload,
			projectFK:         projectFK,
			kandangID:         kandangID,
			totalPrice:        totalPrice,
			shippingCostTotal: shippingCostTotal,
		})
	}

	updatedExpenses := make(map[uint64]struct{})

	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, transfer, items, expenseDate, expeditionNonstockID, transfer.MovementNumber, uint(supplierID))
		if err != nil {
			return err
		}
		if err := b.linkExpenseNonstocksToDetails(ctx, expenseDetail, items); err != nil {
			return err
		}
		if expenseDetail != nil && expenseDetail.Id != 0 {
			updatedExpenses[uint64(expenseDetail.Id)] = struct{}{}
		}
	}

	if len(updatedExpenses) > 0 {
		actorID := uint(1) // Default actor
		if err := b.markExpensesUpdated(ctx, updatedExpenses, actorID); err != nil {
			return err
		}
	}

	return nil
}
