package fifo_stock_v2

import (
	"context"
	"encoding/json"
	"fmt"
	"math"
	"time"

	"gorm.io/gorm"
)

func (s *fifoStockV2Service) Recalculate(ctx context.Context, req RecalculateRequest) (*RecalculateResult, error) {
	result := &RecalculateResult{Drifts: make([]WarehouseDrift, 0)}

	err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
		hash := requestHash(map[string]any{
			"product_warehouse_ids": req.ProductWarehouseIDs,
			"flag_group_codes":      req.FlagGroupCodes,
			"as_of":                 req.AsOf,
			"fix_drift":             req.FixDrift,
		})
		logRow, reused, err := s.beginOperation(
			tx,
			OperationRecalculate,
			req.IdempotencyKey,
			hash,
			0,
			"RECALCULATE",
			"",
			0,
		)
		if err != nil {
			return err
		}
		if reused {
			if len(logRow.ResultPayload) == 0 {
				return fmt.Errorf("idempotent recalculate has empty payload")
			}
			if err := json.Unmarshal(logRow.ResultPayload, result); err != nil {
				return err
			}
			return nil
		}
		if logRow != nil {
			defer func() {
				if err != nil {
					s.failOperation(tx, logRow, err)
				}
			}()
		}

		warehouseIDs, err := s.resolveRecalculateWarehouseIDs(ctx, tx, req.ProductWarehouseIDs)
		if err != nil {
			return err
		}
		groupCodes, err := s.resolveRecalculateGroupCodes(ctx, tx, req.FlagGroupCodes)
		if err != nil {
			return err
		}

		for _, warehouseID := range warehouseIDs {
			expected := 0.0
			for _, flagGroup := range groupCodes {
				available, calcErr := s.calculateWarehouseAvailableForGroup(ctx, tx, warehouseID, flagGroup, req.AsOf)
				if calcErr != nil {
					return calcErr
				}
				expected += available
			}

			actual, actualErr := s.loadWarehouseQty(ctx, tx, warehouseID)
			if actualErr != nil {
				return actualErr
			}

			delta := expected - actual
			result.Checked++
			if math.Abs(delta) < 1e-6 {
				continue
			}

			drift := WarehouseDrift{
				ProductWarehouseID: warehouseID,
				ExpectedQty:        expected,
				ActualQty:          actual,
				Delta:              delta,
			}
			result.Drifts = append(result.Drifts, drift)

			if req.FixDrift {
				if err := s.adjustProductWarehouseQty(tx, warehouseID, delta); err != nil {
					return err
				}
				result.Fixed++
			}
		}

		if err := s.finishOperation(tx, logRow, result); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return result, nil
}

func (s *fifoStockV2Service) resolveRecalculateWarehouseIDs(ctx context.Context, tx *gorm.DB, provided []uint) ([]uint, error) {
	if len(provided) > 0 {
		return provided, nil
	}
	var ids []uint
	err := tx.WithContext(ctx).Table("product_warehouses").Select("id").Order("id ASC").Scan(&ids).Error
	if err != nil {
		return nil, err
	}
	return ids, nil
}

func (s *fifoStockV2Service) resolveRecalculateGroupCodes(ctx context.Context, tx *gorm.DB, provided []string) ([]string, error) {
	if len(provided) > 0 {
		return provided, nil
	}
	var groups []string
	err := tx.WithContext(ctx).
		Table("fifo_stock_v2_flag_groups").
		Select("code").
		Where("is_active = TRUE").
		Order("priority ASC, code ASC").
		Scan(&groups).Error
	if err != nil {
		return nil, err
	}
	return groups, nil
}

func (s *fifoStockV2Service) calculateWarehouseAvailableForGroup(
	ctx context.Context,
	tx *gorm.DB,
	warehouseID uint,
	flagGroupCode string,
	asOf *time.Time,
) (float64, error) {
	rows, err := s.gatherRows(ctx, tx, GatherRequest{
		FlagGroupCode:      flagGroupCode,
		Lane:               LaneStockable,
		ProductWarehouseID: warehouseID,
		AsOf:               asOf,
		Limit:              50000,
	})
	if err != nil {
		return 0, err
	}
	total := 0.0
	for _, row := range rows {
		total += row.AvailableQuantity
	}
	return total, nil
}

func (s *fifoStockV2Service) loadWarehouseQty(ctx context.Context, tx *gorm.DB, warehouseID uint) (float64, error) {
	type row struct {
		Qty float64 `gorm:"column:qty"`
	}
	var out row
	err := tx.WithContext(ctx).
		Table("product_warehouses").
		Select("COALESCE(qty,0) AS qty").
		Where("id = ?", warehouseID).
		Take(&out).Error
	if err != nil {
		return 0, err
	}
	return out.Qty, nil
}
