package repositories

import (
	"context"
	"fmt"
	"strconv"
	"strings"

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

type AdjustmentStockRepository interface {
	CreateOne(ctx context.Context, data *entity.AdjustmentStock, modifier func(*gorm.DB) *gorm.DB) error
	GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error)
	GetByIDForUpdate(ctx context.Context, id uint) (*entity.AdjustmentStock, error)
	FindKandangIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (uint, error)
	FindProductIDByProductWarehouseID(ctx context.Context, productWarehouseID uint) (uint, error)
	FindRoutesByFunctionCode(ctx context.Context, productID uint, functionCode string) ([]AdjustmentRouteResolution, error)
	LoadDownstreamDependencies(ctx context.Context, stockableType string, stockableIDs []uint) ([]AdjustmentDownstreamDependency, error)
	FindAyamSourceProductWarehouse(ctx context.Context, warehouseID uint, projectFlockKandangID uint) (*entity.ProductWarehouse, error)
	IsAyamProduct(ctx context.Context, productID uint) (bool, error)
	CountActiveConsumeAllocationsByUsable(ctx context.Context, usableType string, usableID uint) (int64, error)
	UpdateTotalQty(ctx context.Context, id uint, qty float64) error
	UpdatePairedAdjustmentID(ctx context.Context, id uint, pairedID uint) error
	DeleteStockLogsByAdjustmentID(ctx context.Context, adjustmentID uint) error
	DeleteAdjustmentByID(ctx context.Context, id uint) error
	ResyncProjectFlockPopulationUsage(ctx context.Context, projectFlockKandangID uint) error
	FindHistory(ctx context.Context, filter AdjustmentHistoryFilter, modifier func(*gorm.DB) *gorm.DB) ([]*entity.AdjustmentStock, int64, error)
	WithTx(tx *gorm.DB) AdjustmentStockRepository
	DB() *gorm.DB
	GenerateSequentialNumber(ctx context.Context, prefix string) (string, error)
}

type AdjustmentRouteResolution struct {
	FlagGroupCode       string `gorm:"column:flag_group_code"`
	Lane                string `gorm:"column:lane"`
	FunctionCode        string `gorm:"column:function_code"`
	SourceTable         string `gorm:"column:source_table"`
	LegacyTypeKey       string `gorm:"column:legacy_type_key"`
	AllowPendingDefault bool   `gorm:"column:allow_pending_default"`
}

type AdjustmentHistoryFilter struct {
	ProductID       uint
	WarehouseID     uint
	TransactionType string
	FunctionCode    string
	ScopeRestrict   bool
	ScopeIDs        []uint
	Offset          int
	Limit           int
}

type AdjustmentDownstreamDependency struct {
	UsableType    string `gorm:"column:usable_type"`
	UsableID      uint64 `gorm:"column:usable_id"`
	FunctionCode  string `gorm:"column:function_code"`
	FlagGroupCode string `gorm:"column:flag_group_code"`
}

type adjustmentStockRepositoryImpl struct {
	db *gorm.DB
}

func NewAdjustmentStockRepository(db *gorm.DB) AdjustmentStockRepository {
	return &adjustmentStockRepositoryImpl{db: db}
}

func (r *adjustmentStockRepositoryImpl) CreateOne(ctx context.Context, data *entity.AdjustmentStock, modifier func(*gorm.DB) *gorm.DB) error {
	q := r.db.WithContext(ctx)
	if modifier != nil {
		q = modifier(q)
	}
	return q.Create(data).Error
}

func (r *adjustmentStockRepositoryImpl) GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) {
	var record entity.AdjustmentStock
	q := r.db.WithContext(ctx)
	if modifier != nil {
		q = modifier(q)
	}
	err := q.First(&record, id).Error
	if err != nil {
		return nil, err
	}
	return &record, nil
}

func (r *adjustmentStockRepositoryImpl) GetByIDForUpdate(ctx context.Context, id uint) (*entity.AdjustmentStock, error) {
	var record entity.AdjustmentStock
	if err := r.db.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("id = ?", id).
		Take(&record).Error; err != nil {
		return nil, err
	}
	return &record, nil
}

func (r *adjustmentStockRepositoryImpl) FindKandangIDByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (uint, error) {
	type pfkRow struct {
		KandangID uint `gorm:"column:kandang_id"`
	}

	var pfk pfkRow
	err := r.db.WithContext(ctx).
		Table("project_flock_kandangs pfk").
		Select("pfk.kandang_id").
		Where("pfk.id = ?", projectFlockKandangID).
		Where("pfk.closed_at IS NULL").
		Take(&pfk).Error
	if err != nil {
		return 0, err
	}
	return pfk.KandangID, nil
}

func (r *adjustmentStockRepositoryImpl) FindProductIDByProductWarehouseID(ctx context.Context, productWarehouseID uint) (uint, error) {
	type productRow struct {
		ProductID uint `gorm:"column:product_id"`
	}
	var row productRow
	if err := r.db.WithContext(ctx).
		Table("product_warehouses").
		Select("product_id").
		Where("id = ?", productWarehouseID).
		Take(&row).Error; err != nil {
		return 0, err
	}
	return row.ProductID, nil
}

func (r *adjustmentStockRepositoryImpl) FindRoutesByFunctionCode(
	ctx context.Context,
	productID uint,
	functionCode string,
) ([]AdjustmentRouteResolution, error) {
	var rows []AdjustmentRouteResolution
	err := r.db.WithContext(ctx).
		Table("fifo_stock_v2_route_rules rr").
		Select("rr.flag_group_code, rr.lane, rr.function_code, rr.source_table, rr.legacy_type_key, rr.allow_pending_default").
		Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
		Where("rr.is_active = TRUE").
		Where("rr.function_code = ?", functionCode).
		Where(`
			EXISTS (
				SELECT 1
				FROM flags f
				JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
				WHERE f.flagable_type = ?
				  AND f.flagable_id = ?
				  AND fm.flag_group_code = rr.flag_group_code
			)
		`, entity.FlagableTypeProduct, productID).
		Order("CASE WHEN rr.source_table = 'adjustment_stocks' THEN 0 ELSE 1 END ASC").
		Order("rr.id ASC").
		Find(&rows).Error
	if err != nil {
		return nil, err
	}
	return rows, nil
}

func (r *adjustmentStockRepositoryImpl) LoadDownstreamDependencies(
	ctx context.Context,
	stockableType string,
	stockableIDs []uint,
) ([]AdjustmentDownstreamDependency, error) {
	if strings.TrimSpace(stockableType) == "" || len(stockableIDs) == 0 {
		return nil, nil
	}

	var rows []AdjustmentDownstreamDependency
	err := r.db.WithContext(ctx).
		Table("stock_allocations").
		Select("usable_type, usable_id, COALESCE(function_code,'') AS function_code, COALESCE(flag_group_code,'') AS flag_group_code").
		Where("stockable_type = ?", strings.ToUpper(strings.TrimSpace(stockableType))).
		Where("stockable_id IN ?", stockableIDs).
		Where("status = ?", entity.StockAllocationStatusActive).
		Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where("deleted_at IS NULL").
		Where(
			"(usable_type <> ? OR EXISTS (SELECT 1 FROM project_chickins pc WHERE pc.id = stock_allocations.usable_id AND pc.deleted_at IS NULL))",
			"PROJECT_CHICKIN",
		).
		Group("usable_type, usable_id, function_code, flag_group_code").
		Scan(&rows).Error
	if err != nil {
		return nil, err
	}

	return rows, nil
}

func (r *adjustmentStockRepositoryImpl) FindAyamSourceProductWarehouse(
	ctx context.Context,
	warehouseID uint,
	projectFlockKandangID uint,
) (*entity.ProductWarehouse, error) {
	var sourcePW entity.ProductWarehouse
	err := r.db.WithContext(ctx).
		Model(&entity.ProductWarehouse{}).
		Where("project_flock_kandang_id = ?", projectFlockKandangID).
		Where(`
			EXISTS (
				SELECT 1
				FROM flags f
				JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
				WHERE f.flagable_type = ?
				  AND f.flagable_id = product_warehouses.product_id
				  AND fm.flag_group_code = ?
			)
		`, entity.FlagableTypeProduct, "AYAM").
		Order(gorm.Expr("CASE WHEN warehouse_id = ? THEN 0 ELSE 1 END ASC", warehouseID)).
		Order("id ASC").
		Take(&sourcePW).Error
	if err != nil {
		return nil, err
	}
	return &sourcePW, nil
}

func (r *adjustmentStockRepositoryImpl) IsAyamProduct(ctx context.Context, productID uint) (bool, error) {
	if productID == 0 {
		return false, nil
	}

	var count int64
	if err := r.db.WithContext(ctx).
		Table("flags f").
		Joins("JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.flag_group_code = ? AND fm.is_active = TRUE", "AYAM").
		Where("f.flagable_type = ?", entity.FlagableTypeProduct).
		Where("f.flagable_id = ?", productID).
		Count(&count).Error; err != nil {
		return false, err
	}

	return count > 0, nil
}

func (r *adjustmentStockRepositoryImpl) CountActiveConsumeAllocationsByUsable(
	ctx context.Context,
	usableType string,
	usableID uint,
) (int64, error) {
	if strings.TrimSpace(usableType) == "" || usableID == 0 {
		return 0, nil
	}

	var count int64
	err := r.db.WithContext(ctx).
		Table("stock_allocations").
		Where("usable_type = ?", strings.ToUpper(strings.TrimSpace(usableType))).
		Where("usable_id = ?", usableID).
		Where("status = ?", entity.StockAllocationStatusActive).
		Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
		Where("deleted_at IS NULL").
		Count(&count).Error
	if err != nil {
		return 0, err
	}

	return count, nil
}

func (r *adjustmentStockRepositoryImpl) UpdateTotalQty(ctx context.Context, id uint, qty float64) error {
	return r.db.WithContext(ctx).
		Model(&entity.AdjustmentStock{}).
		Where("id = ?", id).
		Update("total_qty", qty).Error
}

func (r *adjustmentStockRepositoryImpl) UpdatePairedAdjustmentID(ctx context.Context, id uint, pairedID uint) error {
	return r.db.WithContext(ctx).
		Model(&entity.AdjustmentStock{}).
		Where("id = ?", id).
		Update("paired_adjustment_id", pairedID).Error
}

func (r *adjustmentStockRepositoryImpl) DeleteStockLogsByAdjustmentID(ctx context.Context, adjustmentID uint) error {
	return r.db.WithContext(ctx).
		Where("loggable_type = ? AND loggable_id = ?", string(utils.StockLogTypeAdjustment), adjustmentID).
		Delete(&entity.StockLog{}).Error
}

func (r *adjustmentStockRepositoryImpl) DeleteAdjustmentByID(ctx context.Context, id uint) error {
	return r.db.WithContext(ctx).
		Where("id = ?", id).
		Delete(&entity.AdjustmentStock{}).Error
}

func (r *adjustmentStockRepositoryImpl) ResyncProjectFlockPopulationUsage(ctx context.Context, projectFlockKandangID uint) error {
	if projectFlockKandangID == 0 {
		return nil
	}

	idsSubquery := `
		SELECT pfp.id
		FROM project_flock_populations pfp
		JOIN project_chickins pc ON pc.id = pfp.project_chickin_id
		WHERE pc.project_flock_kandang_id = ?
	`

	updateWithAlloc := `
		UPDATE project_flock_populations p
		SET total_used_qty = COALESCE(a.used, 0)
		FROM (
			SELECT stockable_id, SUM(qty) AS used
			FROM stock_allocations
			WHERE stockable_type = 'PROJECT_FLOCK_POPULATION'
			  AND status = 'ACTIVE'
			  AND allocation_purpose = 'CONSUME'
			GROUP BY stockable_id
		) a
		WHERE p.id = a.stockable_id
		  AND p.id IN (` + idsSubquery + `)
	`

	resetMissing := `
		UPDATE project_flock_populations p
		SET total_used_qty = 0
		WHERE p.id IN (` + idsSubquery + `)
			AND NOT EXISTS (
				SELECT 1
				FROM stock_allocations sa
				WHERE sa.stockable_type = 'PROJECT_FLOCK_POPULATION'
				  AND sa.status = 'ACTIVE'
				  AND sa.allocation_purpose = 'CONSUME'
				  AND sa.stockable_id = p.id
			)
	`

	db := r.db.WithContext(ctx)
	if err := db.Exec(updateWithAlloc, projectFlockKandangID).Error; err != nil {
		return err
	}
	if err := db.Exec(resetMissing, projectFlockKandangID).Error; err != nil {
		return err
	}
	return nil
}

func (r *adjustmentStockRepositoryImpl) FindHistory(
	ctx context.Context,
	filter AdjustmentHistoryFilter,
	modifier func(*gorm.DB) *gorm.DB,
) ([]*entity.AdjustmentStock, int64, error) {
	q := r.db.WithContext(ctx).Model(&entity.AdjustmentStock{}).
		Preload("ProductWarehouse").
		Preload("ProductWarehouse.Product").
		Preload("ProductWarehouse.Warehouse").
		Preload("ProductWarehouse.Warehouse.Location").
		Preload("ProductWarehouse.ProjectFlockKandang").
		Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock").
		Preload("StockLog.CreatedUser")

	if modifier != nil {
		q = modifier(q)
	}

	if filter.ScopeRestrict {
		q = q.Joins("JOIN product_warehouses pw_scope ON pw_scope.id = adjustment_stocks.product_warehouse_id").
			Joins("JOIN warehouses w_scope ON w_scope.id = pw_scope.warehouse_id")
		if len(filter.ScopeIDs) == 0 {
			q = q.Where("1 = 0")
		} else {
			q = q.Where("w_scope.location_id IN ?", filter.ScopeIDs)
		}
	}

	if filter.ProductID > 0 || filter.WarehouseID > 0 {
		q = q.Joins("JOIN product_warehouses pw_filter ON pw_filter.id = adjustment_stocks.product_warehouse_id")
		if filter.ProductID > 0 {
			q = q.Where("pw_filter.product_id = ?", filter.ProductID)
		}
		if filter.WarehouseID > 0 {
			q = q.Where("pw_filter.warehouse_id = ?", filter.WarehouseID)
		}
	}

	if strings.TrimSpace(filter.TransactionType) != "" {
		q = q.Where("UPPER(adjustment_stocks.transaction_type) = ?", strings.ToUpper(strings.TrimSpace(filter.TransactionType)))
	}

	if strings.TrimSpace(filter.FunctionCode) != "" {
		q = q.Where("adjustment_stocks.function_code = ?", strings.ToUpper(strings.TrimSpace(filter.FunctionCode)))
	}

	var total int64
	if err := q.Count(&total).Error; err != nil {
		return nil, 0, err
	}

	var rows []entity.AdjustmentStock
	if err := q.Offset(filter.Offset).Limit(filter.Limit).Order("created_at DESC").Find(&rows).Error; err != nil {
		return nil, 0, err
	}

	result := make([]*entity.AdjustmentStock, len(rows))
	for i := range rows {
		result[i] = &rows[i]
	}
	return result, total, nil
}

func (r *adjustmentStockRepositoryImpl) WithTx(tx *gorm.DB) AdjustmentStockRepository {
	return &adjustmentStockRepositoryImpl{db: tx}
}

func (r *adjustmentStockRepositoryImpl) DB() *gorm.DB {
	return r.db
}

func (r *adjustmentStockRepositoryImpl) GenerateSequentialNumber(ctx context.Context, prefix string) (string, error) {
	var values []string
	err := r.db.WithContext(ctx).
		Model(&entity.AdjustmentStock{}).
		Where(fmt.Sprintf("%s ILIKE ?", "adj_number"), prefix+"%").
		Select("adj_number").
		Order(fmt.Sprintf("%s DESC", "adj_number")).
		Limit(20).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Pluck("adj_number", &values).Error
	if err != nil {
		return "", err
	}

	next := 1
	for _, value := range values {
		if number, ok := parseNumericSuffix(value, prefix); ok {
			next = number + 1
			break
		}
	}

	const maxAttempts = 20
	for attempt := 0; attempt < maxAttempts; attempt++ {
		candidate := fmt.Sprintf("%s%0*d", prefix, 5, next)
		exists, err := r.numberExists(ctx, r.db, candidate)
		if err != nil {
			return "", err
		}
		if !exists {
			return candidate, nil
		}
		next++
	}

	return "", fmt.Errorf("unable to generate unique %s", "adj_number")
}

func (r *adjustmentStockRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, value string) (bool, error) {
	var count int64
	if err := db.WithContext(ctx).
		Model(&entity.AdjustmentStock{}).
		Where(fmt.Sprintf("%s = ?", "adj_number"), value).
		Count(&count).Error; err != nil {
		return false, err
	}
	return count > 0, nil
}

func parseNumericSuffix(value, prefix string) (int, bool) {
	if !strings.HasPrefix(value, prefix) {
		return 0, false
	}
	suffix := strings.TrimPrefix(value, prefix)
	if suffix == "" {
		return 0, false
	}
	trimmed := strings.TrimLeft(suffix, "0")
	if trimmed == "" {
		trimmed = "0"
	}
	number, err := strconv.Atoi(trimmed)
	if err != nil {
		return 0, false
	}
	return number, true
}
