package repositories

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

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

type PurchaseRepository interface {
	repository.BaseRepository[entity.Purchase]
	CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
	CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error
	UpdatePricing(ctx context.Context, purchaseID uint, updates []PurchasePricingUpdate) error
	UpdateReceivingDetails(ctx context.Context, purchaseID uint, updates []PurchaseReceivingUpdate) error
	DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error
	NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
	NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
	BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error
	SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
	GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
	GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error)
	GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error)
}

type PurchaseRepositoryImpl struct {
	*repository.BaseRepositoryImpl[entity.Purchase]
}

func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
	return &PurchaseRepositoryImpl{
		BaseRepositoryImpl: repository.NewBaseRepository[entity.Purchase](db),
	}
}

func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
	db := r.DB().WithContext(ctx)

	//ambil dari base repository
	if err := db.Create(purchase).Error; err != nil {
		return err
	}

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

	for _, item := range items {
		item.PurchaseId = purchase.Id
	}

	if err := db.Create(&items).Error; err != nil {
		return err
	}
	return nil
}

func (r *PurchaseRepositoryImpl) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error {
	if purchaseID == 0 {
		return nil
	}

	query := `
WITH latest_pfk AS (
	SELECT pfk.id, pfk.kandang_id
	FROM project_flock_kandangs pfk
	JOIN (
		SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at
		FROM approvals
		WHERE approvable_type = 'PROJECT_FLOCKS'
		ORDER BY approvable_id, action_at DESC
	) latest_approval ON latest_approval.approvable_id = pfk.project_flock_id
	WHERE LOWER(latest_approval.step_name) = LOWER('Aktif')
)
UPDATE purchase_items pi
SET project_flock_kandang_id = lp.id
FROM warehouses w
JOIN latest_pfk lp ON lp.kandang_id = w.kandang_id
WHERE pi.purchase_id = ?
  AND pi.project_flock_kandang_id IS NULL
  AND pi.warehouse_id = w.id;
`
	return r.DB().WithContext(ctx).Exec(query, purchaseID).Error
}

func (r *PurchaseRepositoryImpl) SoftDeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
	if len(projectFlockKandangIDs) == 0 {
		return nil
	}

	return r.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var purchaseIDs []uint
		query := `
SELECT pi.purchase_id
FROM purchase_items pi
WHERE pi.project_flock_kandang_id IN (?)
GROUP BY pi.purchase_id
HAVING COUNT(*) = COUNT(CASE WHEN pi.project_flock_kandang_id IN (?) THEN 1 END)
`
		if err := tx.Raw(query, projectFlockKandangIDs, projectFlockKandangIDs).Scan(&purchaseIDs).Error; err != nil {
			return err
		}

		now := time.Now().UTC()
		if len(purchaseIDs) > 0 {
			if err := tx.Model(&entity.Purchase{}).
				Where("id IN (?) AND deleted_at IS NULL", purchaseIDs).
				Update("deleted_at", now).Error; err != nil {
				return err
			}
			if err := tx.Where("purchase_id IN (?)", purchaseIDs).Delete(&entity.PurchaseItem{}).Error; err != nil {
				return err
			}
		}

		deleteItems := tx.Where("project_flock_kandang_id IN (?)", projectFlockKandangIDs)
		if len(purchaseIDs) > 0 {
			deleteItems = deleteItems.Where("purchase_id NOT IN (?)", purchaseIDs)
		}
		return deleteItems.Delete(&entity.PurchaseItem{}).Error
	})
}

func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error {
	if len(items) == 0 {
		return nil
	}

	for _, item := range items {
		if item == nil {
			continue
		}
		item.PurchaseId = purchaseID
	}

	return r.DB().WithContext(ctx).Create(&items).Error
}

func (r *PurchaseRepositoryImpl) purchaseItemExists(ctx context.Context, purchaseID uint, itemID uint) (bool, error) {
	var count int64
	if err := r.DB().WithContext(ctx).
		Model(&entity.PurchaseItem{}).
		Where("purchase_id = ? AND id = ?", purchaseID, itemID).
		Count(&count).Error; err != nil {
		return false, err
	}
	return count > 0, nil
}

type PurchasePricingUpdate struct {
	ItemID     uint
	ProductID  *uint
	Price      float64
	TotalPrice float64
	Quantity   *float64
	TotalQty   *float64
}

type PurchaseReceivingUpdate struct {
	ItemID                uint
	ReceivedDate          *time.Time
	TravelNumber          *string
	TravelDocumentPath    *string
	VehicleNumber         *string
	ClearVehicleNumber    bool
	ReceivedQty           *float64
	WarehouseID           *uint
	ProductWarehouseID    *uint
	ClearProductWarehouse bool
}

func (r *PurchaseRepositoryImpl) UpdatePricing(
	ctx context.Context,
	purchaseID uint,
	updates []PurchasePricingUpdate,
) error {
	if len(updates) == 0 {
		return errors.New("pricing updates cannot be empty")
	}

	db := r.DB().WithContext(ctx)

	for _, upd := range updates {
		data := map[string]interface{}{
			"price":       upd.Price,
			"total_price": upd.TotalPrice,
		}
		if upd.ProductID != nil {
			data["product_id"] = *upd.ProductID
		}
		if upd.Quantity != nil {
			data["sub_qty"] = *upd.Quantity
		}
		if upd.TotalQty != nil {
			data["total_qty"] = *upd.TotalQty
		}

		result := db.Model(&entity.PurchaseItem{}).
			Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
			Updates(data)
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			exists, err := r.purchaseItemExists(ctx, purchaseID, upd.ItemID)
			if err != nil {
				return err
			}
			if !exists {
				return gorm.ErrRecordNotFound
			}
		}
	}

	return nil
}

func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
	ctx context.Context,
	purchaseID uint,
	updates []PurchaseReceivingUpdate,
) error {
	if len(updates) == 0 {
		return errors.New("receiving updates cannot be empty")
	}

	db := r.DB().WithContext(ctx)

	for _, upd := range updates {
		data := map[string]interface{}{}

		if upd.ReceivedDate != nil {
			data["received_date"] = upd.ReceivedDate
		}
		if upd.TravelNumber != nil {
			data["travel_number"] = upd.TravelNumber
		}
		if upd.TravelDocumentPath != nil {
			data["travel_number_docs"] = upd.TravelDocumentPath
		}
		if upd.VehicleNumber != nil {
			data["vehicle_number"] = upd.VehicleNumber
		} else if upd.ClearVehicleNumber {
			data["vehicle_number"] = ""
		}
		if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
			data["warehouse_id"] = upd.WarehouseID
		}

		if upd.ProductWarehouseID != nil {
			data["product_warehouse_id"] = *upd.ProductWarehouseID
		} else if upd.ClearProductWarehouse {
			data["product_warehouse_id"] = gorm.Expr("NULL")
		}

		if len(data) == 0 {
			continue
		}

		result := db.Model(&entity.PurchaseItem{}).
			Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
			Updates(data)
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			exists, err := r.purchaseItemExists(ctx, purchaseID, upd.ItemID)
			if err != nil {
				return err
			}
			if !exists {
				return gorm.ErrRecordNotFound
			}
		}
	}

	return nil
}

func (r *PurchaseRepositoryImpl) GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error) {
	const unassignedSQL = "'" + exportprogress.UnassignedKandangName + "'"
	subQuery := r.DB().WithContext(ctx).
		Table("purchases AS p").
		Select(`
			DISTINCT p.id AS purchase_id,
			'Purchases' AS module,
			COALESCE(pf_explicit.flock_name, pf_active.flock_name, kandang_loc.name, warehouse_loc.name, 'Unknown Farm') AS farm_name,
			COALESCE(k_explicit.name, k_active.name, wk.name, `+unassignedSQL+`, 'Unknown Kandang') AS kandang_name,
			CAST(DATE(p.po_date) AS TEXT) AS activity_date
		`).
		Joins("JOIN purchase_items pi ON pi.purchase_id = p.id").
		Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
		Joins("LEFT JOIN project_flock_kandangs pfk_explicit ON pfk_explicit.id = pi.project_flock_kandang_id").
		Joins("LEFT JOIN project_flocks pf_explicit ON pf_explicit.id = pfk_explicit.project_flock_id").
		Joins("LEFT JOIN kandangs k_explicit ON k_explicit.id = pfk_explicit.kandang_id").
		Joins("LEFT JOIN project_flock_kandangs pfk_active ON pfk_active.kandang_id = w.kandang_id AND pfk_active.closed_at IS NULL").
		Joins("LEFT JOIN project_flocks pf_active ON pf_active.id = pfk_active.project_flock_id").
		Joins("LEFT JOIN kandangs k_active ON k_active.id = pfk_active.kandang_id").
		Joins("LEFT JOIN kandangs wk ON wk.id = w.kandang_id").
		Joins("LEFT JOIN locations kandang_loc ON kandang_loc.id = COALESCE(k_explicit.location_id, k_active.location_id, wk.location_id)").
		Joins("LEFT JOIN locations warehouse_loc ON warehouse_loc.id = w.location_id").
		Where("p.deleted_at IS NULL").
		Where("p.po_date IS NOT NULL").
		Where("DATE(p.po_date) >= DATE(?)", startDate).
		Where("DATE(p.po_date) <= DATE(?)", endDate)

	if restrict {
		if len(allowedLocationIDs) == 0 {
			return []exportprogress.Row{}, nil
		}
		subQuery = subQuery.Where("w.location_id IN ?", allowedLocationIDs)
	}

	type progressRowResult struct {
		Module       string
		FarmName     string
		KandangName  string
		ActivityDate string
		Count        int
	}
	scanned := make([]progressRowResult, 0)
	err := r.DB().WithContext(ctx).
		Table("(?) AS progress_rows", subQuery).
		Select("module, farm_name, kandang_name, activity_date, COUNT(*) AS count").
		Group("module, farm_name, kandang_name, activity_date").
		Order("activity_date ASC, farm_name ASC, kandang_name ASC").
		Scan(&scanned).Error
	if err != nil {
		return nil, err
	}

	rows := make([]exportprogress.Row, 0, len(scanned))
	for _, item := range scanned {
		activityDate, err := exportprogress.ParseActivityDate(item.ActivityDate)
		if err != nil {
			return nil, err
		}
		rows = append(rows, exportprogress.Row{
			Module:       item.Module,
			FarmName:     item.FarmName,
			KandangName:  item.KandangName,
			ActivityDate: activityDate,
			Count:        item.Count,
		})
	}
	return rows, nil
}

func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error {
	if len(itemIDs) == 0 {
		return errors.New("itemIDs cannot be empty")
	}

	return r.DB().WithContext(ctx).
		Where("purchase_id = ? AND id IN ?", purchaseID, itemIDs).
		Delete(&entity.PurchaseItem{}).Error
}

func (r *PurchaseRepositoryImpl) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) {
	return r.generateSequentialNumber(ctx, tx, "pr_number", utils.PurchasePRNumberPrefix, utils.PurchaseNumberPadding)
}

func (r *PurchaseRepositoryImpl) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
	return r.generateSequentialNumber(ctx, tx, "po_number", utils.PurchasePONumberPrefix, utils.PurchaseNumberPadding)
}

func (r *PurchaseRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) {
	db := tx
	if db == nil {
		db = r.DB()
	}

	var values []string
	err := db.WithContext(ctx).
		Model(&entity.Purchase{}).
		Where(fmt.Sprintf("%s ILIKE ?", column), prefix+"%").
		Select(column).
		Order(fmt.Sprintf("%s DESC", column)).
		Limit(20).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Pluck(column, &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, padding, next)
		exists, err := r.numberExists(ctx, db, column, candidate)
		if err != nil {
			return "", err
		}
		if !exists {
			return candidate, nil
		}
		next++
	}

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

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

func (r *PurchaseRepositoryImpl) GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) {

	return r.GetItemsByWarehouseKandang(ctx, projectFlockID)
}

func (r *PurchaseRepositoryImpl) GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) {
	var items []entity.PurchaseItem

	var kandangIDs []uint
	err := r.DB().WithContext(ctx).
		Table("project_flock_kandangs").
		Where("project_flock_id = ?", projectFlockID).
		Pluck("kandang_id", &kandangIDs).Error

	if err != nil {
		return nil, err
	}

	if len(kandangIDs) == 0 {
		return []entity.PurchaseItem{}, nil
	}

	err = r.DB().WithContext(ctx).
		Preload("Product").
		Preload("Product.Flags").
		Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id").
		Where("warehouses.kandang_id IN ?", kandangIDs).
		Find(&items).Error

	return items, err
}

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
}
