package repository

import (
	"context"
	"errors"
	"fmt"

	"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gorm.io/gorm"
)

type ProductWarehouseRepository interface {
	repository.BaseRepository[entity.ProductWarehouse]
	ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error)
	IsProductExist(ctx context.Context, productId uint) (bool, error)
	IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error)
	ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
	ExistsByID(ctx context.Context, id uint) (bool, error)
	GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
	FindByProductWarehouseAndPfk(ctx context.Context, productID uint, warehouseID uint, projectFlockKandangID *uint) (*entity.ProductWarehouse, error)
	GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
	GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
	GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
	GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
	ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error)
	ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
	AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
	GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
	IdExists(ctx context.Context, id uint) (bool, error)
	CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
	EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error)
	GetByProductWarehouseAndProjectFlockKandang(ctx context.Context, productId, warehouseId, projectFlockKandangId uint) (*entity.ProductWarehouse, error)
	DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
}

type ProductWarehouseRepositoryImpl struct {
	*repository.BaseRepositoryImpl[entity.ProductWarehouse]
}

func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository {
	return &ProductWarehouseRepositoryImpl{
		BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](db),
	}
}

func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
	return repository.Exists[entity.Product](ctx, r.DB(), productId)
}
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
	return repository.Exists[entity.Warehouse](ctx, r.DB(), warehouseId)
}

func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
	return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
}

func (r *ProductWarehouseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
	return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
}

func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) {
	var count int64
	query := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
		Where("product_id = ? AND warehouse_id = ?", productId, warehouseId)
	if excludeID != nil {
		query = query.Where("id != ?", *excludeID)
	}
	if err := query.Count(&count).Error; err != nil {
		return false, err
	}
	return count > 0, nil
}

func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
	var count int64
	if err := r.DB().WithContext(ctx).
		Model(&entity.ProductWarehouse{}).
		Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).
		Count(&count).Error; err != nil {
		return false, err
	}
	return count > 0, nil
}

func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) {
	warehouseIsKandang, err := r.isKandangWarehouse(ctx, warehouseId)
	if err != nil {
		return nil, err
	}

	if warehouseIsKandang {
		if productWarehouse, err := r.findOpenKandangOwnedWarehouse(ctx, productId, warehouseId); err == nil {
			return productWarehouse, nil
		} else if !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}

		return r.findSharedWarehouse(ctx, productId, warehouseId)
	}

	if productWarehouse, err := r.findSharedWarehouse(ctx, productId, warehouseId); err == nil {
		return productWarehouse, nil
	} else if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	return r.findOpenKandangOwnedWarehouse(ctx, productId, warehouseId)
}

func (r *ProductWarehouseRepositoryImpl) FindByProductWarehouseAndPfk(ctx context.Context, productID uint, warehouseID uint, projectFlockKandangID *uint) (*entity.ProductWarehouse, error) {
	var productWarehouse entity.ProductWarehouse

	err := r.DB().WithContext(ctx).
		Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NOT DISTINCT FROM ?", productID, warehouseID, projectFlockKandangID).
		First(&productWarehouse).Error

	if err != nil {
		return nil, err
	}

	return &productWarehouse, nil
}

func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
	var productWarehouses []entity.ProductWarehouse
	q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
		Joins("JOIN products ON products.id = product_warehouses.product_id").
		Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
		Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
		Order("product_warehouses.id DESC")

	// preload relations so nested Product and Warehouse are populated
	err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
	if err != nil {
		return nil, err
	}
	return productWarehouses, nil
}

func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) {
	var productWarehouse entity.ProductWarehouse
	query := r.DB()
	if db != nil {
		query = db
	}
	fmt.Println(warehouseId)
	err := query.WithContext(ctx).
		Model(&entity.ProductWarehouse{}).
		Joins("JOIN products ON products.id = product_warehouses.product_id").
		Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
		Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
		Order("product_warehouses.created_at DESC").
		Preload("Product").Preload("Warehouse").
		First(&productWarehouse).Error
	if err != nil {
		return nil, err
	}
	return &productWarehouse, nil
}

func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB {
	if len(flags) == 0 {
		return db
	}

	fallbackCategoryCodes := utils.LegacyProductCategoryCodesForFlags(flags)

	db = db.
		Joins("JOIN products p_flag ON p_flag.id = product_warehouses.product_id").
		Joins("LEFT JOIN product_categories pc_flag ON pc_flag.id = p_flag.product_category_id")

	actualFlagFilter := `
		EXISTS (
			SELECT 1
			FROM flags f_flag
			WHERE f_flag.flagable_id = p_flag.id
			  AND f_flag.flagable_type = ?
			  AND f_flag.name IN ?
		)
	`

	if len(fallbackCategoryCodes) == 0 {
		return db.Where(actualFlagFilter, entity.FlagableTypeProduct, flags).Distinct()
	}

	return db.
		Where(
			`(`+actualFlagFilter+`) OR (
				NOT EXISTS (
					SELECT 1
					FROM flags f_any
					WHERE f_any.flagable_id = p_flag.id
					  AND f_any.flagable_type = ?
				)
				AND pc_flag.code IN ?
			)`,
			entity.FlagableTypeProduct,
			flags,
			entity.FlagableTypeProduct,
			fallbackCategoryCodes,
		).
		Distinct()
}

func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error {
	if len(deltas) == 0 {
		return nil
	}

	base := r.DB().WithContext(ctx)
	if modifier != nil {
		base = modifier(base)
	}

	for id, delta := range deltas {
		if delta == 0 {
			continue
		}
		if err := base.Model(&entity.ProductWarehouse{}).
			Where("id = ?", id).
			Update("qty", gorm.Expr("COALESCE(qty,0) + ?", delta)).Error; err != nil {
			return err
		}
	}
	return nil
}

func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error {
	if len(affected) == 0 {
		return nil
	}

	ids := make([]uint, 0, len(affected))
	for id := range affected {
		ids = append(ids, id)
	}

	var emptyIDs []uint
	if err := r.DB().WithContext(ctx).
		Model(&entity.ProductWarehouse{}).
		Where("id IN ? AND COALESCE(qty,0) <= 0", ids).
		Pluck("id", &emptyIDs).Error; err != nil {
		return err
	}
	if len(emptyIDs) == 0 {
		return nil
	}

	var inUseIDs []uint
	if err := r.DB().WithContext(ctx).
		Model(&entity.PurchaseItem{}).
		Where("product_warehouse_id IN ?", emptyIDs).
		Distinct().
		Pluck("product_warehouse_id", &inUseIDs).Error; err != nil {
		return err
	}
	if len(inUseIDs) > 0 {
		inUse := make(map[uint]struct{}, len(inUseIDs))
		for _, id := range inUseIDs {
			inUse[id] = struct{}{}
		}
		filtered := make([]uint, 0, len(emptyIDs))
		for _, id := range emptyIDs {
			if _, exists := inUse[id]; !exists {
				filtered = append(filtered, id)
			}
		}
		emptyIDs = filtered
	}
	if len(emptyIDs) == 0 {
		return nil
	}

	if err := r.DB().WithContext(ctx).
		Model(&entity.PurchaseItem{}).
		Where("product_warehouse_id IN ?", emptyIDs).
		Update("product_warehouse_id", nil).Error; err != nil {
		return err
	}

	if err := r.DB().WithContext(ctx).
		Where("id IN ?", emptyIDs).
		Delete(&entity.ProductWarehouse{}).Error; err != nil {
		return err
	}

	return nil
}

func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
	ctx context.Context,
	productID uint,
	warehouseID uint,
	projectFlockKandangID *uint,
	createdBy uint,
) (uint, error) {
	record, err := r.FindByProductWarehouseAndPfk(ctx, productID, warehouseID, projectFlockKandangID)
	if err == nil {
		return record.Id, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return 0, err
	}

	entity := &entity.ProductWarehouse{
		ProductId:             productID,
		WarehouseId:           warehouseID,
		ProjectFlockKandangId: projectFlockKandangID,
		Quantity:              0,
		// CreatedBy:   uint(createdBy),
	}
	// if entity.CreatedBy == 0 {
	// 	entity.CreatedBy = 1
	// }

	if err := r.CreateOne(ctx, entity, nil); err != nil {
		return 0, err
	}
	return entity.Id, nil
}

func (r *ProductWarehouseRepositoryImpl) isKandangWarehouse(ctx context.Context, warehouseID uint) (bool, error) {
	var kandangID *uint
	if err := r.DB().WithContext(ctx).
		Table("warehouses").
		Select("kandang_id").
		Where("id = ?", warehouseID).
		Scan(&kandangID).Error; err != nil {
		return false, err
	}
	return kandangID != nil && *kandangID != 0, nil
}

func (r *ProductWarehouseRepositoryImpl) findOpenKandangOwnedWarehouse(ctx context.Context, productID uint, warehouseID uint) (*entity.ProductWarehouse, error) {
	var productWarehouse entity.ProductWarehouse
	err := r.DB().WithContext(ctx).
		Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NOT NULL", productID, warehouseID).
		Order("id DESC").
		Preload("ProjectFlockKandang").
		First(&productWarehouse).Error
	if err != nil {
		return nil, err
	}
	if productWarehouse.ProjectFlockKandang != nil && productWarehouse.ProjectFlockKandang.ClosedAt == nil {
		return &productWarehouse, nil
	}
	return nil, gorm.ErrRecordNotFound
}

func (r *ProductWarehouseRepositoryImpl) findSharedWarehouse(ctx context.Context, productID uint, warehouseID uint) (*entity.ProductWarehouse, error) {
	var productWarehouse entity.ProductWarehouse
	if err := r.DB().WithContext(ctx).
		Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NULL", productID, warehouseID).
		Preload("ProjectFlockKandang").
		First(&productWarehouse).Error; err != nil {
		return nil, err
	}
	return &productWarehouse, nil
}

func (r *ProductWarehouseRepositoryImpl) GetByProductWarehouseAndProjectFlockKandang(
	ctx context.Context,
	productId uint,
	warehouseId uint,
	projectFlockKandangId uint,
) (*entity.ProductWarehouse, error) {
	var productWarehouse entity.ProductWarehouse
	if err := r.DB().WithContext(ctx).
		Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id = ?", productId, warehouseId, projectFlockKandangId).
		First(&productWarehouse).Error; err != nil {
		return nil, err
	}
	return &productWarehouse, nil
}

func (r *ProductWarehouseRepositoryImpl) DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
	if len(projectFlockKandangIDs) == 0 {
		return nil
	}
	return r.DB().WithContext(ctx).
		Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
		Delete(&entity.ProductWarehouse{}).Error
}

func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
	var productWarehouse entity.ProductWarehouse
	err := r.DB().WithContext(ctx).
		Preload("Product").
		Preload("Warehouse").
		Preload("Warehouse.Area").
		Preload("Warehouse.Location").
		Preload("ProjectFlockKandang").
		Preload("ProjectFlockKandang.ProjectFlock").
		First(&productWarehouse, id).Error
	if err != nil {
		return nil, err
	}
	return &productWarehouse, nil
}

func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
	var product entity.Product
	err := r.DB().WithContext(ctx).
		Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
		Where("product_categories.code = ?", categoryCode).
		First(&product).Error
	if err != nil {
		return nil, err
	}
	return &product, nil
}

func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) {
	var productWarehouses []entity.ProductWarehouse
	err := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
		Joins("JOIN products ON products.id = product_warehouses.product_id").
		Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
		Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
		Order("product_warehouses.id DESC").
		Preload("Product").
		Preload("Product.ProductCategory").
		Preload("Product.Uom").
		Preload("Warehouse").
		Find(&productWarehouses).Error
	if err != nil {
		return nil, err
	}
	return productWarehouses, nil
}

func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) {
	var product entity.Product
	err := r.DB().WithContext(ctx).
		Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
		Where("flags.name = ?", flagName).
		First(&product).Error
	if err != nil {
		return nil, err
	}
	return &product, nil
}

func (r *ProductWarehouseRepositoryImpl) ListProductIDsByFlagPrefixes(ctx context.Context, prefixes []string, visibleStatus *bool) ([]uint, error) {
	if len(prefixes) == 0 {
		return []uint{}, nil
	}

	db := r.DB().WithContext(ctx).
		Model(&entity.Product{}).
		Distinct("products.id").
		Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", entity.FlagableTypeProduct)

	applied := false
	for _, prefix := range prefixes {
		if prefix == "" {
			continue
		}
		like := prefix + "%"
		if !applied {
			db = db.Where("flags.name ILIKE ?", like)
			applied = true
			continue
		}
		db = db.Or("flags.name ILIKE ?", like)
	}

	if visibleStatus != nil {
		db = db.Where("products.is_visible = ?", *visibleStatus)
	}

	var ids []uint
	if err := db.Pluck("products.id", &ids).Error; err != nil {
		return nil, err
	}
	return ids, nil
}
