package service

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

	"github.com/gofiber/fiber/v2"
	commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

func (s *transferService) CreateSystemTransfer(ctx context.Context, req *SystemTransferRequest) (*entity.StockTransfer, error) {
	if req == nil {
		return nil, fmt.Errorf("system transfer request is required")
	}
	if strings.TrimSpace(req.TransferReason) == "" {
		return nil, fmt.Errorf("transfer reason is required")
	}
	if req.TransferDate.IsZero() {
		return nil, fmt.Errorf("transfer date is required")
	}
	if req.SourceWarehouseID == 0 || req.DestinationWarehouseID == 0 {
		return nil, fmt.Errorf("source and destination warehouse are required")
	}
	if req.SourceWarehouseID == req.DestinationWarehouseID {
		return nil, fmt.Errorf("source and destination warehouse must be different")
	}
	if req.ActorID == 0 {
		return nil, fmt.Errorf("actor id is required")
	}

	if err := s.validateTransferWarehousesAndProducts(ctx, req.SourceWarehouseID, req.DestinationWarehouseID, req.Products); err != nil {
		return nil, err
	}

	var result *entity.StockTransfer
	err := s.StockTransferRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		movementResult, err := s.createTransferMovement(ctx, tx, req)
		if err != nil {
			return err
		}
		result = movementResult.Transfer
		return nil
	})
	if err != nil {
		return nil, err
	}

	return result, nil
}

func (s *transferService) DeleteSystemTransfer(ctx context.Context, id uint, actorID uint) error {
	if id == 0 {
		return fmt.Errorf("transfer id is required")
	}
	if actorID == 0 {
		return fmt.Errorf("actor id is required")
	}

	var deletedDetails []entity.StockTransferDetail
	err := s.StockTransferRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var err error
		deletedDetails, err = s.deleteTransferCore(ctx, tx, uint64(id), actorID)
		return err
	})
	if err != nil {
		return err
	}

	if len(deletedDetails) > 0 && s.ExpenseBridge != nil {
		if err := s.ExpenseBridge.OnItemsDeleted(ctx, uint64(id), deletedDetails); err != nil {
			s.Log.Errorf("Failed to cleanup transfer expense link for transfer_id=%d: %+v", id, err)
			return fiber.NewError(fiber.StatusInternalServerError, "Transfer berhasil dihapus, namun sinkronisasi expense gagal. Silakan cek modul expense")
		}
	}

	return nil
}

func (s *transferService) validateTransferWarehousesAndProducts(
	ctx context.Context,
	sourceWarehouseID uint,
	destinationWarehouseID uint,
	products []SystemTransferProduct,
) error {
	if len(products) == 0 {
		return fmt.Errorf("transfer products are required")
	}

	pwIDs := make([]uint, 0, len(products))
	for _, product := range products {
		if product.ProductID == 0 {
			return fmt.Errorf("product id is required")
		}
		if product.ProductQty <= 0 {
			return fmt.Errorf("product qty must be greater than 0 for product %d", product.ProductID)
		}

		sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
			ctx, product.ProductID, sourceWarehouseID,
		)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, sourceWarehouseID))
			}
			s.Log.Errorf("Failed to fetch product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, sourceWarehouseID, err)
			return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengecek stok produk")
		}
		if sourcePW.Quantity < product.ProductQty {
			return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty))
		}
		pwIDs = append(pwIDs, sourcePW.Id)
	}

	if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(ctx, s.StockTransferRepo.DB(), pwIDs); err != nil {
		return err
	}

	destPfkID, err := s.getActiveProjectFlockKandangID(ctx, destinationWarehouseID)
	if err != nil {
		return err
	}
	if destPfkID == 0 {
		return nil
	}

	projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(ctx, destPfkID)
	if err != nil {
		s.Log.Errorf("Failed to fetch project flock kandang by ID %d: %+v", destPfkID, err)
		return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
	}
	if projectFlockKandang.ClosedAt != nil {
		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02")))
	}

	return nil
}

func (s *transferService) createTransferMovement(
	ctx context.Context,
	tx *gorm.DB,
	req *SystemTransferRequest,
) (*transferMovementResult, error) {
	if tx == nil {
		return nil, fmt.Errorf("transaction is required")
	}

	stockTransferRepoTX := s.StockTransferRepo.WithTx(tx)
	stockTransferDetailRepoTX := s.StockTransferDetailRepo.WithTx(tx)
	productWarehouseRepoTX := rProductWarehouse.NewProductWarehouseRepository(tx)
	stockLogsRepoTX := rStockLogs.NewStockLogRepository(tx)

	movementNumber := strings.TrimSpace(req.MovementNumber)
	if movementNumber == "" {
		var err error
		movementNumber, err = s.StockTransferRepo.GenerateMovementNumber(ctx)
		if err != nil {
			s.Log.Errorf("Failed to generate movement number: %+v", err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor transfer")
		}
	}

	entityTransfer := &entity.StockTransfer{
		FromWarehouseId: uint64(req.SourceWarehouseID),
		ToWarehouseId:   uint64(req.DestinationWarehouseID),
		Reason:          req.TransferReason,
		TransferDate:    req.TransferDate,
		MovementNumber:  movementNumber,
		CreatedBy:       uint64(req.ActorID),
	}
	if err := stockTransferRepoTX.CreateOne(ctx, entityTransfer, nil); err != nil {
		return nil, err
	}

	details := make([]*entity.StockTransferDetail, 0, len(req.Products))
	detailMap := make(map[uint64]*entity.StockTransferDetail, len(req.Products))
	for _, product := range req.Products {
		sourcePW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
			ctx, product.ProductID, req.SourceWarehouseID,
		)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
			}
			s.Log.Errorf("Failed to fetch source product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.SourceWarehouseID, err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data stok gudang asal")
		}

		destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
			ctx, product.ProductID, req.DestinationWarehouseID,
		)
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			s.Log.Errorf("Failed to fetch dest product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.DestinationWarehouseID, err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data stok gudang tujuan")
		}
		if errors.Is(err, gorm.ErrRecordNotFound) {
			projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, req.DestinationWarehouseID)
			if err != nil {
				return nil, err
			}

			var pfkID *uint
			if projectFlockKandangID > 0 {
				pfkID = &projectFlockKandangID
			}

			destPW = &entity.ProductWarehouse{
				ProductId:             product.ProductID,
				WarehouseId:           req.DestinationWarehouseID,
				Quantity:              0,
				ProjectFlockKandangId: pfkID,
			}
			if err := productWarehouseRepoTX.CreateOne(ctx, destPW, nil); err != nil {
				s.Log.Errorf("Failed to create product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.DestinationWarehouseID, err)
				return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat data stok gudang tujuan")
			}
		}

		detail := &entity.StockTransferDetail{
			StockTransferId: entityTransfer.Id,
			ProductId:       uint64(product.ProductID),
			SourceProductWarehouseID: func() *uint64 {
				id := uint64(sourcePW.Id)
				return &id
			}(),
			UsageQty:   0,
			PendingQty: 0,
			DestProductWarehouseID: func() *uint64 {
				id := uint64(destPW.Id)
				return &id
			}(),
			TotalQty:  0,
			TotalUsed: 0,
		}
		details = append(details, detail)
		detailMap[uint64(product.ProductID)] = detail
	}

	if err := stockTransferDetailRepoTX.CreateMany(ctx, details, nil); err != nil {
		return nil, err
	}

	flagGroupByProduct := make(map[uint]string, len(req.Products))
	for _, product := range req.Products {
		detail := detailMap[uint64(product.ProductID)]
		if detail == nil || detail.SourceProductWarehouseID == nil || detail.DestProductWarehouseID == nil {
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Data transfer detail tidak valid")
		}

		flagGroupCode, ok := flagGroupByProduct[product.ProductID]
		if !ok {
			var err error
			flagGroupCode, err = s.resolveTransferFlagGroup(ctx, tx, product.ProductID)
			if err != nil {
				return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("FIFO v2 route tidak ditemukan untuk produk %d: %v", product.ProductID, err))
			}
			flagGroupByProduct[product.ProductID] = flagGroupCode
		}

		if err := tx.Model(&entity.StockTransferDetail{}).
			Where("id = ?", detail.Id).
			Updates(map[string]interface{}{
				"usage_qty":   product.ProductQty,
				"pending_qty": 0,
				"total_qty":   product.ProductQty,
			}).Error; err != nil {
			s.Log.Errorf("Failed to update transfer detail seed fields for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
		}

		asOf := req.TransferDate
		if _, err := s.FifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
			FlagGroupCode:      flagGroupCode,
			ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
			AsOf:               &asOf,
			Tx:                 tx,
		}); err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
		}
		if _, err := s.FifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
			FlagGroupCode:      flagGroupCode,
			ProductWarehouseID: uint(*detail.DestProductWarehouseID),
			AsOf:               &asOf,
			Tx:                 tx,
		}); err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow stok tujuan untuk produk %d. Error: %v", product.ProductID, err))
		}

		type usageSnapshot struct {
			UsageQty   float64 `gorm:"column:usage_qty"`
			PendingQty float64 `gorm:"column:pending_qty"`
		}
		var usage usageSnapshot
		if err := tx.WithContext(ctx).
			Table("stock_transfer_details").
			Select("usage_qty, pending_qty").
			Where("id = ?", detail.Id).
			Take(&usage).Error; err != nil {
			s.Log.Errorf("Failed to read transfer usage snapshot detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data tracking")
		}
		outUsageQty := usage.UsageQty
		outPendingQty := usage.PendingQty
		if outPendingQty > 1e-6 {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal", product.ProductID))
		}

		stockLogDecrease := &entity.StockLog{
			ProductWarehouseId: uint(*detail.SourceProductWarehouseID),
			CreatedBy:          req.ActorID,
			Increase:           0,
			Decrease:           outUsageQty,
			LoggableType:       string(utils.StockLogTypeTransfer),
			LoggableId:         uint(detail.Id),
			Notes:              req.StockLogNotes,
		}
		stockLogs, err := stockLogsRepoTX.GetByProductWarehouse(ctx, uint(*detail.SourceProductWarehouseID), 1)
		if err != nil {
			s.Log.Errorf("Failed to get stock logs: %+v", err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
		}
		if len(stockLogs) > 0 {
			latestStockLog := stockLogs[0]
			stockLogDecrease.Stock = latestStockLog.Stock - stockLogDecrease.Decrease
		} else {
			stockLogDecrease.Stock -= stockLogDecrease.Decrease
		}
		if err := stockLogsRepoTX.CreateOne(ctx, stockLogDecrease, nil); err != nil {
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
		}

		stockLogIncrease := &entity.StockLog{
			ProductWarehouseId: uint(*detail.DestProductWarehouseID),
			CreatedBy:          req.ActorID,
			Increase:           outUsageQty,
			Decrease:           0,
			LoggableType:       string(utils.StockLogTypeTransfer),
			LoggableId:         uint(detail.Id),
			Notes:              req.StockLogNotes,
		}
		stockLogs, err = stockLogsRepoTX.GetByProductWarehouse(ctx, uint(*detail.DestProductWarehouseID), 1)
		if err != nil {
			s.Log.Errorf("Failed to get stock logs: %+v", err)
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
		}
		if len(stockLogs) > 0 {
			latestStockLog := stockLogs[0]
			stockLogIncrease.Stock = latestStockLog.Stock + stockLogIncrease.Increase
		} else {
			stockLogIncrease.Stock += stockLogIncrease.Increase
		}
		if err := stockLogsRepoTX.CreateOne(ctx, stockLogIncrease, nil); err != nil {
			return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
		}
	}

	return &transferMovementResult{
		Transfer:    entityTransfer,
		DetailByPID: detailMap,
	}, nil
}

func (s *transferService) deleteTransferCore(
	ctx context.Context,
	tx *gorm.DB,
	transferID uint64,
	actorID uint,
) ([]entity.StockTransferDetail, error) {
	stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)

	var transfer entity.StockTransfer
	if err := tx.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("id = ?", transferID).
		Where("deleted_at IS NULL").
		Take(&transfer).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", transferID))
		}
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data transfer")
	}

	var details []entity.StockTransferDetail
	if err := tx.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("stock_transfer_id = ?", transfer.Id).
		Where("deleted_at IS NULL").
		Order("id ASC").
		Find(&details).Error; err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil detail transfer")
	}
	if len(details) == 0 {
		return nil, fiber.NewError(fiber.StatusBadRequest, "Transfer tidak memiliki detail produk")
	}

	detailIDs := make([]uint64, 0, len(details))
	for _, detail := range details {
		detailIDs = append(detailIDs, detail.Id)
	}
	if err := s.ensureDeletePolicyForDownstreamConsumption(ctx, tx, detailIDs); err != nil {
		return nil, err
	}

	type reflowKey struct {
		flagGroupCode      string
		productWarehouseID uint
	}
	destReflows := make(map[reflowKey]struct{})

	for _, detail := range details {
		if detail.SourceProductWarehouseID == nil || *detail.SourceProductWarehouseID == 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Detail transfer %d tidak memiliki source product warehouse valid", detail.Id))
		}
		if detail.DestProductWarehouseID == nil || *detail.DestProductWarehouseID == 0 {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Detail transfer %d tidak memiliki destination product warehouse valid", detail.Id))
		}

		flagGroupCode, err := s.resolveTransferFlagGroup(ctx, tx, uint(detail.ProductId))
		if err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("FIFO v2 route tidak ditemukan untuk produk %d: %v", detail.ProductId, err))
		}

		rollbackRes, err := s.FifoStockV2Svc.Rollback(ctx, commonSvc.FifoStockV2RollbackRequest{
			ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
			Usable: commonSvc.FifoStockV2Ref{
				ID:            uint(detail.Id),
				LegacyTypeKey: fifo.UsableKeyStockTransferOut.String(),
				FunctionCode:  "STOCK_TRANSFER_OUT",
			},
			Reason: fmt.Sprintf("transfer delete #%s", transfer.MovementNumber),
			Tx:     tx,
		})
		if err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 transfer detail %d: %v", detail.Id, err))
		}

		releasedQty := 0.0
		if rollbackRes != nil {
			releasedQty = rollbackRes.ReleasedQty
		}
		if detail.UsageQty > 1e-6 && releasedQty < detail.UsageQty-1e-6 {
			return nil, fiber.NewError(
				fiber.StatusBadRequest,
				fmt.Sprintf("Rollback FIFO v2 source transfer detail %d tidak lengkap. Dibutuhkan %.3f, terlepas %.3f", detail.Id, detail.UsageQty, releasedQty),
			)
		}

		if releasedQty > 1e-6 {
			if err := s.appendStockLog(
				ctx,
				stockLogRepoTx,
				uint(*detail.SourceProductWarehouseID),
				actorID,
				releasedQty,
				0,
				uint(detail.Id),
				fmt.Sprintf("TRANSFER DELETE #%s", transfer.MovementNumber),
			); err != nil {
				return nil, err
			}
		}

		destDecreaseQty := detail.TotalQty
		if destDecreaseQty <= 1e-6 {
			destDecreaseQty = detail.UsageQty
		}
		if destDecreaseQty > 1e-6 {
			if err := s.appendStockLog(
				ctx,
				stockLogRepoTx,
				uint(*detail.DestProductWarehouseID),
				actorID,
				0,
				destDecreaseQty,
				uint(detail.Id),
				fmt.Sprintf("TRANSFER DELETE #%s", transfer.MovementNumber),
			); err != nil {
				return nil, err
			}
		}

		destReflows[reflowKey{
			flagGroupCode:      flagGroupCode,
			productWarehouseID: uint(*detail.DestProductWarehouseID),
		}] = struct{}{}
	}

	now := time.Now().UTC()
	if err := tx.WithContext(ctx).
		Where("stock_transfer_detail_id IN ?", detailIDs).
		Delete(&entity.StockTransferDeliveryItem{}).Error; err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus item delivery transfer")
	}
	if err := tx.WithContext(ctx).
		Model(&entity.StockTransferDelivery{}).
		Where("stock_transfer_id = ?", transfer.Id).
		Where("deleted_at IS NULL").
		Updates(map[string]any{
			"deleted_at": now,
			"updated_at": now,
		}).Error; err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus delivery transfer")
	}
	if err := tx.WithContext(ctx).
		Model(&entity.StockTransferDetail{}).
		Where("id IN ?", detailIDs).
		Where("deleted_at IS NULL").
		Updates(map[string]any{
			"deleted_at": now,
			"updated_at": now,
		}).Error; err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus detail transfer")
	}

	asOf := transfer.TransferDate
	for key := range destReflows {
		if _, err := s.FifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
			FlagGroupCode:      key.flagGroupCode,
			ProductWarehouseID: key.productWarehouseID,
			AsOf:               &asOf,
			Tx:                 tx,
		}); err != nil {
			return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow stok tujuan saat delete transfer: %v", err))
		}
	}

	if err := tx.WithContext(ctx).
		Model(&entity.StockTransfer{}).
		Where("id = ?", transfer.Id).
		Where("deleted_at IS NULL").
		Updates(map[string]any{
			"deleted_at": now,
			"updated_at": now,
		}).Error; err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus transfer")
	}

	return details, nil
}
