package service

import (
	"errors"
	"strings"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"github.com/sirupsen/logrus"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
	kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gorm.io/gorm"
)

type ProductWarehouseService interface {
	GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductWarehouse, error)
}

type productWarehouseService struct {
	Log         *logrus.Logger
	Validate    *validator.Validate
	Repository  repository.ProductWarehouseRepository
	KandangRepo kandangrepo.KandangRepository
}

const stockModeExcludeChickin = "exclude_chickin"

func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate, kandangRepo kandangrepo.KandangRepository) ProductWarehouseService {
	return &productWarehouseService{
		Log:         utils.Log,
		Validate:    validate,
		Repository:  repo,
		KandangRepo: kandangRepo,
	}
}

func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
	return db.
		Preload("Product.Flags").
		Preload("Product").
		Preload("Product.Uom").
		Preload("Warehouse").
		Preload("Warehouse.Location").
		Preload("Warehouse.Area").
		Preload("Warehouse.Kandang").
		Preload("ProjectFlockKandang").
		Preload("ProjectFlockKandang.ProjectFlock").
		Preload("ProjectFlockKandang.Chickins")
}

func applyWarehouseSelectionFilter(db *gorm.DB, kandangID, locationID uint) *gorm.DB {
	switch {
	case kandangID != 0 && locationID != 0:
		return db.Where(
			"w_scope.location_id = ? AND (w_scope.type = ? OR w_scope.kandang_id = ?)",
			locationID,
			"LOKASI",
			kandangID,
		)
	case kandangID != 0:
		return db.Where("w_scope.kandang_id = ?", kandangID)
	case locationID != 0:
		return db.Where("w_scope.location_id = ?", locationID)
	default:
		return db
	}
}

func applyAvailableOnlyFilter(db *gorm.DB, availableOnly bool) *gorm.DB {
	if !availableOnly {
		return db
	}
	return db.Where("COALESCE(product_warehouses.qty, 0) > 0")
}

func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

	applyScope := true
	if params.TransferContext == utils.TransferContextInventoryTransfer {
		applyScope = !m.HasPermission(c, m.P_TransferCreateOne)
	}
	var scope m.ScopeFilter
	var err error
	if applyScope {
		scope, err = m.ResolveLocationScope(c, s.Repository.DB())
		if err != nil {
			return nil, 0, err
		}
	}

	if params.ProductId > 0 {
		isProductExist, err := s.Repository.IsProductExist(c.Context(), params.ProductId)
		if err != nil {
			return nil, 0, err
		}
		if !isProductExist {
			return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
		}
	}

	if params.WarehouseId > 0 {
		isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), params.WarehouseId)
		if err != nil {
			return nil, 0, err
		}
		if !isWarehouseExist {
			return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
		}
	}

	if params.KandangId > 0 {
		isKandangExist, err := s.KandangRepo.IdExists(c.Context(), params.KandangId)
		if err != nil {
			return nil, 0, err
		}
		if !isKandangExist {
			return nil, 0, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
		}
	}

	offset := (params.Page - 1) * params.Limit

	var marketingTypes []string
	if params.Type != "" {
		marketingTypes = utils.ParseQueryArray(params.Type)
		for _, t := range marketingTypes {
			if !utils.IsValidMarketingType(t) {
				return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing type")
			}
		}
	}

	cleanFlags := utils.ParseFlags(params.Flags)

	productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
		db = s.withRelations(db)

		db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id").
			Where("w_scope.deleted_at IS NULL")
		if applyScope {
			if scope.Restrict {
				if len(scope.IDs) == 0 {
					return db.Where("1 = 0")
				}
				db = db.Where("w_scope.location_id IN ?", scope.IDs)
			}
		}

		if params.ProductId != 0 {
			db = db.Where("product_id = ?", params.ProductId)
		}

		db = applyAvailableOnlyFilter(db, params.AvailableOnly)

		db = applyWarehouseSelectionFilter(db, params.KandangId, params.LocationId)

		if params.WarehouseId != 0 {
			db = db.Where("warehouse_id = ?", params.WarehouseId)
		}

		if strings.TrimSpace(params.Search) != "" {
			searchPattern := "%" + strings.TrimSpace(params.Search) + "%"
			db = db.Where(
				`(
					EXISTS (
						SELECT 1
						FROM products p_search
						WHERE p_search.id = product_warehouses.product_id
						  AND p_search.name ILIKE ?
					)
					OR w_scope.name ILIKE ?
				)`,
				searchPattern,
				searchPattern,
			)
		}

		if len(marketingTypes) > 0 {
			flagSet := make(map[string]struct{})
			for _, t := range marketingTypes {
				switch t {
				case string(utils.MarketingTypeAyamPullet):
					flagSet[string(utils.FlagDOC)] = struct{}{}
					flagSet[string(utils.FlagPullet)] = struct{}{}
					flagSet[string(utils.FlagLayer)] = struct{}{}
				case string(utils.MarketingTypeAyam):
					flagSet[string(utils.FlagAyamAfkir)] = struct{}{}
					flagSet[string(utils.FlagAyamCulling)] = struct{}{}
					flagSet[string(utils.FlagAyamMati)] = struct{}{}
				case string(utils.MarketingTypeTelur):
					flagSet[string(utils.FlagTelur)] = struct{}{}
					flagSet[string(utils.FlagTelurUtuh)] = struct{}{}
					flagSet[string(utils.FlagTelurPecah)] = struct{}{}
					flagSet[string(utils.FlagTelurPutih)] = struct{}{}
					flagSet[string(utils.FlagTelurRetak)] = struct{}{}
				case string(utils.MarketingTypeTrading):
					flagSet[string(utils.FlagPakan)] = struct{}{}
					flagSet[string(utils.FlagPreStarter)] = struct{}{}
					flagSet[string(utils.FlagStarter)] = struct{}{}
					flagSet[string(utils.FlagFinisher)] = struct{}{}
					flagSet[string(utils.FlagOVK)] = struct{}{}
					flagSet[string(utils.FlagObat)] = struct{}{}
					flagSet[string(utils.FlagVitamin)] = struct{}{}
					flagSet[string(utils.FlagKimia)] = struct{}{}
					flagSet[string(utils.FlagEkspedisi)] = struct{}{}
				}
			}
			if len(flagSet) > 0 {
				flags := make([]string, 0, len(flagSet))
				for f := range flagSet {
					flags = append(flags, f)
				}
				db = s.Repository.ApplyFlagsFilter(db, flags)
			}
		}

		if len(cleanFlags) > 0 {
			db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
		}

		return db.Order("product_warehouses.id DESC")
	})

	if err != nil {
		s.Log.Errorf("Failed to get productWarehouses: %+v", err)
		return nil, 0, err
	}

	productWarehouses, err = s.applyTransferAvailableQty(c, params, productWarehouses)
	if err != nil {
		return nil, 0, err
	}
	return productWarehouses, total, nil
}

func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) {
	applyScope := true
	if c.Query(utils.TransferContextKey, "") == utils.TransferContextInventoryTransfer {
		applyScope = !m.HasPermission(c, m.P_TransferCreateOne)
	}
	var scope m.ScopeFilter
	var err error
	if applyScope {
		scope, err = m.ResolveLocationScope(c, s.Repository.DB())
		if err != nil {
			return nil, err
		}
	}

	productWarehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
		db = s.withRelations(db)
		db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id").
			Where("w_scope.deleted_at IS NULL")
		if applyScope {
			if scope.Restrict {
				if len(scope.IDs) == 0 {
					return db.Where("1 = 0")
				}
				db = db.Where("w_scope.location_id IN ?", scope.IDs)
			}
		}
		return db
	})
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found")
	}
	if err != nil {
		s.Log.Errorf("Failed get productWarehouse by id: %+v", err)
		return nil, err
	}
	return productWarehouse, nil
}

func (s productWarehouseService) applyTransferAvailableQty(c *fiber.Ctx, params *validation.Query, rows []entity.ProductWarehouse) ([]entity.ProductWarehouse, error) {
	if len(rows) == 0 {
		return rows, nil
	}
	if params == nil ||
		params.TransferContext != utils.TransferContextInventoryTransfer ||
		params.StockMode != stockModeExcludeChickin {
		return rows, nil
	}

	ayamPWIDs := make([]uint, 0)
	for i := range rows {
		if isAyamProductByFlags(rows[i].Product.Flags) {
			ayamPWIDs = append(ayamPWIDs, rows[i].Id)
		}
	}
	if len(ayamPWIDs) == 0 {
		return rows, nil
	}

	type populationRemainingRow struct {
		ProductWarehouseID uint    `gorm:"column:product_warehouse_id"`
		RemainingQty       float64 `gorm:"column:remaining_qty"`
	}

	var populationRows []populationRemainingRow
	if err := s.Repository.DB().WithContext(c.Context()).
		Table("project_flock_populations pfp").
		Select("pfp.product_warehouse_id, COALESCE(SUM(GREATEST(pfp.total_qty - pfp.total_used_qty, 0)), 0) AS remaining_qty").
		Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
		Where("pfp.product_warehouse_id IN ?", ayamPWIDs).
		Where("pfp.deleted_at IS NULL").
		Where("pc.deleted_at IS NULL").
		Group("pfp.product_warehouse_id").
		Scan(&populationRows).Error; err != nil {
		s.Log.Errorf("Failed to resolve chickin population remaining for transfer stock filter: %+v", err)
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve transfer stock availability")
	}

	populationRemainingByPW := make(map[uint]float64, len(populationRows))
	for _, row := range populationRows {
		populationRemainingByPW[row.ProductWarehouseID] = row.RemainingQty
	}

	filtered := make([]entity.ProductWarehouse, 0, len(rows))
	for i := range rows {
		row := rows[i]
		if !isAyamProductByFlags(row.Product.Flags) {
			filtered = append(filtered, row)
			continue
		}

		available := row.Quantity - populationRemainingByPW[row.Id]
		if available < 0 {
			available = 0
		}
		row.AvailableQty = &available

		if available <= 0 {
			continue
		}

		filtered = append(filtered, row)
	}

	return filtered, nil
}

func isAyamProductByFlags(flags []entity.Flag) bool {
	for _, flag := range flags {
		if utils.CanonicalFlagType(strings.TrimSpace(flag.Name)) == utils.FlagAyam {
			return true
		}
	}
	return false
}
