package main

import (
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"log"
	"os"
	"sort"
	"strings"
	"text/tabwriter"
	"time"

	"github.com/go-playground/validator/v10"
	"github.com/sirupsen/logrus"
	"gitlab.com/mbugroup/lti-api.git/internal/common/service"
	"gitlab.com/mbugroup/lti-api.git/internal/config"
	"gitlab.com/mbugroup/lti-api.git/internal/database"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	pwRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	transferRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
	transferSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
	warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
	pfkRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
	stockLogRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
	"gorm.io/gorm"
)

const (
	transferReasonPrefix = "PRODUCT_FARM_TRANSFER"
	outputModeTable      = "table"
	outputModeJSON       = "json"
)

// commandOptions holds all parsed CLI flags.
type commandOptions struct {
	Apply           bool
	RollbackRunID   string
	LocationID      uint
	LocationName    string
	TransferDate    time.Time
	TransferDateRaw string
	AllLocations    bool
	Output          string
	ActorID         uint
	RunID           string
}

// farmWarehouseInfo holds resolved farm-level warehouse data for a location.
// If FarmCount > 1, the location is invalid (ambiguous target).
type farmWarehouseInfo struct {
	LocationID    uint
	LocationName  string
	FarmCount     int
	WarehouseID   uint   // only reliable when FarmCount == 1
	WarehouseName string // only reliable when FarmCount == 1
}

// kandangStockRow is the raw row loaded from the DB for a single product in a kandang warehouse.
type kandangStockRow struct {
	LocationID          uint
	LocationName        string
	SourceWarehouseID   uint
	SourceWarehouseName string
	ProductWarehouseID  uint
	ProductID           uint
	ProductName         string
	OnHandQty           float64
	AllocatedQty        float64 // sum of ACTIVE CONSUME stock allocations
	LeftoverQty         float64 // OnHandQty - AllocatedQty
}

// transferReportRow is one row in the plan/apply report.
type transferReportRow struct {
	RunID               string  `json:"run_id"`
	LocationID          uint    `json:"location_id"`
	LocationName        string  `json:"location_name"`
	SourceWarehouseID   uint    `json:"source_warehouse_id"`
	SourceWarehouseName string  `json:"source_warehouse_name"`
	FarmWarehouseID     *uint   `json:"farm_warehouse_id,omitempty"`
	FarmWarehouseName   *string `json:"farm_warehouse_name,omitempty"`
	ProductWarehouseID  uint    `json:"product_warehouse_id"`
	ProductID           uint    `json:"product_id"`
	ProductName         string  `json:"product_name"`
	OnHandQty           float64 `json:"on_hand_qty"`
	AllocatedQty        float64 `json:"allocated_qty"`
	Qty                 float64 `json:"qty"` // leftover qty to transfer
	Status              string  `json:"status"`
	Reason              string  `json:"reason,omitempty"`
	TransferID          *uint64 `json:"transfer_id,omitempty"`
	MovementNumber      *string `json:"movement_number,omitempty"`
}

// transferGroup is a single stock transfer: one kandang warehouse → one farm warehouse, with N products.
type transferGroup struct {
	LocationID          uint
	LocationName        string
	SourceWarehouseID   uint
	SourceWarehouseName string
	FarmWarehouseID     uint
	FarmWarehouseName   string
	Rows                []*transferReportRow
}

// applySummary is printed at the end of a plan or apply run.
type applySummary struct {
	RowsPlanned   int `json:"rows_planned"`
	RowsApplied   int `json:"rows_applied"`
	RowsSkipped   int `json:"rows_skipped"`
	RowsError     int `json:"rows_error"`
	RowsFailed    int `json:"rows_failed"`
	GroupsPlanned int `json:"groups_planned"`
	GroupsApplied int `json:"groups_applied"`
}

// rollbackDetailRow is one product line that belongs to a transfer created by a previous run.
type rollbackDetailRow struct {
	RunID               string  `json:"run_id"`
	TransferID          uint64  `json:"transfer_id"`
	MovementNumber      string  `json:"movement_number"`
	LocationName        string  `json:"location_name"`
	SourceWarehouseName string  `json:"source_warehouse_name"`
	FarmWarehouseName   string  `json:"farm_warehouse_name"`
	ProductName         string  `json:"product_name"`
	Qty                 float64 `json:"qty"`
	Status              string  `json:"status"`
	Reason              string  `json:"reason,omitempty"`
}

// systemTransferExecutor abstracts the transfer service so it can be faked in tests.
type systemTransferExecutor interface {
	CreateSystemTransfer(ctx context.Context, req *transferSvc.SystemTransferRequest) (*entity.StockTransfer, error)
	DeleteSystemTransfer(ctx context.Context, id uint, actorID uint) error
}

func main() {
	opts, err := parseFlags()
	if err != nil {
		log.Fatalf("invalid flags: %v", err)
	}

	db := database.Connect(config.DBHost, config.DBName)
	ctx := context.Background()

	// ── Rollback path ────────────────────────────────────────────────────────
	if strings.TrimSpace(opts.RollbackRunID) != "" {
		rows, err := loadRollbackDetails(ctx, db, opts.RollbackRunID)
		if err != nil {
			log.Fatalf("failed to load rollback details: %v", err)
		}
		if len(rows) == 0 {
			fmt.Fprintf(os.Stderr, "no transfers found for run_id=%s\n", opts.RollbackRunID)
			os.Exit(1)
		}
		if !opts.Apply {
			for i := range rows {
				rows[i].Status = "eligible"
			}
			renderRollbackReport(opts.Output, rows)
			return
		}
		if err := executeRollback(ctx, newSystemTransferService(db), rows, opts.ActorID); err != nil {
			log.Fatalf("rollback failed: %v", err)
		}
		renderRollbackReport(opts.Output, rows)
		return
	}

	// ── Plan / Apply path ────────────────────────────────────────────────────
	farmMap, err := loadFarmWarehouseMap(ctx, db, opts)
	if err != nil {
		log.Fatalf("failed to load farm warehouse map: %v", err)
	}

	// Abort early if any in-scope location has multiple farm warehouses and
	// we are about to apply — the ambiguity is too risky to proceed.
	if opts.Apply {
		if msgs := validateFarmWarehouseMap(farmMap); len(msgs) > 0 {
			for _, m := range msgs {
				fmt.Fprintln(os.Stderr, "ERROR:", m)
			}
			log.Fatalf("aborting: resolve multiple-farm-warehouse conflicts before applying")
		}
	}

	stockRows, err := loadKandangLeftoverStocks(ctx, db, opts)
	if err != nil {
		log.Fatalf("failed to load kandang leftover stocks: %v", err)
	}

	reportRows, groups := buildTransferPlan(opts, farmMap, stockRows)

	if !opts.Apply {
		renderTransferReport(opts.Output, reportRows, summarizeReport(reportRows, groups, 0))
		return
	}

	summary, err := executeApply(ctx, newSystemTransferService(db), opts, groups)
	if err != nil {
		log.Fatalf("apply failed: %v", err)
	}
	finalRows := flattenGroups(groups, reportRows)
	summary = summarizeReport(finalRows, groups, summary.GroupsApplied)
	renderTransferReport(opts.Output, finalRows, summary)
}

// ── Flag parsing ──────────────────────────────────────────────────────────────

func parseFlags() (*commandOptions, error) {
	var opts commandOptions
	flag.BoolVar(&opts.Apply, "apply", false, "Apply transfers. If false, run as dry-run and print the plan")
	flag.StringVar(&opts.RollbackRunID, "rollback-run-id", "", "Rollback all transfers created by the provided run_id")
	flag.UintVar(&opts.LocationID, "location-id", 0, "Filter by location id")
	flag.StringVar(&opts.LocationName, "location-name", "", "Filter by exact location name")
	flag.StringVar(&opts.TransferDateRaw, "transfer-date", "", "Transfer date in YYYY-MM-DD format (default: today in Asia/Jakarta)")
	flag.BoolVar(&opts.AllLocations, "all-locations", false, "Allow apply without a location filter (transfers all locations)")
	flag.StringVar(&opts.Output, "output", outputModeTable, "Output format: table or json")
	flag.UintVar(&opts.ActorID, "actor-id", 1, "Actor id used for created/deleted transfers")
	flag.Parse()

	opts.LocationName = strings.TrimSpace(opts.LocationName)
	opts.RollbackRunID = strings.TrimSpace(opts.RollbackRunID)
	opts.Output = strings.ToLower(strings.TrimSpace(opts.Output))
	if opts.Output == "" {
		opts.Output = outputModeTable
	}
	if opts.Output != outputModeTable && opts.Output != outputModeJSON {
		return nil, fmt.Errorf("unsupported --output=%s; must be 'table' or 'json'", opts.Output)
	}

	if opts.LocationID > 0 && opts.LocationName != "" {
		return nil, errors.New("use either --location-id or --location-name, not both")
	}
	if opts.RollbackRunID != "" {
		if opts.LocationID > 0 || opts.LocationName != "" {
			return nil, errors.New("location filters are not supported with --rollback-run-id")
		}
		if opts.TransferDateRaw != "" {
			return nil, errors.New("--transfer-date is not used with --rollback-run-id")
		}
	} else if opts.Apply {
		if !opts.AllLocations && opts.LocationID == 0 && opts.LocationName == "" {
			return nil, errors.New(
				"apply mode requires --location-id, --location-name, or --all-locations for safety; " +
					"use --all-locations only when you have reviewed the dry-run output for all locations",
			)
		}
	}

	jakarta := time.FixedZone("Asia/Jakarta", 7*3600)
	if strings.TrimSpace(opts.TransferDateRaw) == "" {
		opts.TransferDate = normalizeDateOnly(time.Now().In(jakarta))
	} else {
		t, err := time.Parse("2006-01-02", opts.TransferDateRaw)
		if err != nil {
			return nil, fmt.Errorf("invalid --transfer-date: %w", err)
		}
		opts.TransferDate = normalizeDateOnly(t)
	}

	opts.RunID = buildRunID()
	return &opts, nil
}

// ── Service wiring ────────────────────────────────────────────────────────────

func newSystemTransferService(db *gorm.DB) systemTransferExecutor {
	validate := validator.New()
	stockTransferRepo := transferRepo.NewStockTransferRepository(db)
	stockTransferDetailRepo := transferRepo.NewStockTransferDetailRepository(db)
	stockTransferDeliveryRepo := transferRepo.NewStockTransferDeliveryRepository(db)
	stockTransferDeliveryItemRepo := transferRepo.NewStockTransferDeliveryItemRepository(db)
	stockLogsRepo := stockLogRepo.NewStockLogRepository(db)
	productWarehouseRepo := pwRepo.NewProductWarehouseRepository(db)
	warehouseRepository := warehouseRepo.NewWarehouseRepository(db)
	projectFlockKandangRepo := pfkRepo.NewProjectFlockKandangRepository(db)
	projectFlockPopulationRepo := pfkRepo.NewProjectFlockPopulationRepository(db)
	fifoSvc := service.NewFifoStockV2Service(db, logrus.StandardLogger())

	return transferSvc.NewTransferService(
		validate,
		stockTransferRepo,
		stockTransferDetailRepo,
		stockTransferDeliveryRepo,
		stockTransferDeliveryItemRepo,
		stockLogsRepo,
		productWarehouseRepo,
		nil,
		warehouseRepository,
		projectFlockKandangRepo,
		projectFlockPopulationRepo,
		nil,
		fifoSvc,
		nil,
	)
}

// ── DB loading ────────────────────────────────────────────────────────────────

// loadFarmWarehouseMap returns a map keyed by location_id.
// Each entry tells how many LOKASI-type warehouses the location has and which one
// to use (only safe when FarmCount == 1).
func loadFarmWarehouseMap(ctx context.Context, db *gorm.DB, opts *commandOptions) (map[uint]farmWarehouseInfo, error) {
	type row struct {
		LocationID    uint   `gorm:"column:location_id"`
		LocationName  string `gorm:"column:location_name"`
		FarmCount     int    `gorm:"column:farm_count"`
		WarehouseID   uint   `gorm:"column:warehouse_id"`
		WarehouseName string `gorm:"column:warehouse_name"`
	}

	query := db.WithContext(ctx).
		Table("warehouses kw").
		Select(`
			kw.location_id AS location_id,
			l.name         AS location_name,
			COUNT(DISTINCT fw.id) AS farm_count,
			MIN(fw.id)     AS warehouse_id,
			MIN(fw.name)   AS warehouse_name
		`).
		Joins("JOIN locations l ON l.id = kw.location_id").
		Joins(`LEFT JOIN warehouses fw
			ON fw.location_id = kw.location_id
		   AND UPPER(fw.type) = 'LOKASI'
		   AND fw.deleted_at IS NULL`).
		Where("UPPER(kw.type) = 'KANDANG'").
		Where("kw.deleted_at IS NULL").
		Group("kw.location_id, l.name")

	query = applyLocationFilter(query, opts, "kw")

	var rows []row
	if err := query.Scan(&rows).Error; err != nil {
		return nil, err
	}

	result := make(map[uint]farmWarehouseInfo, len(rows))
	for _, r := range rows {
		result[r.LocationID] = farmWarehouseInfo{
			LocationID:    r.LocationID,
			LocationName:  r.LocationName,
			FarmCount:     r.FarmCount,
			WarehouseID:   r.WarehouseID,
			WarehouseName: r.WarehouseName,
		}
	}
	return result, nil
}

// validateFarmWarehouseMap returns one error message per location that has
// more than one farm-level (LOKASI) warehouse.  An empty slice means no issues.
func validateFarmWarehouseMap(m map[uint]farmWarehouseInfo) []string {
	var msgs []string
	for _, info := range m {
		if info.FarmCount > 1 {
			msgs = append(msgs, fmt.Sprintf(
				"location %q (id=%d) has %d LOKASI warehouses; a unique farm warehouse is required — resolve the ambiguity before running",
				info.LocationName, info.LocationID, info.FarmCount,
			))
		}
	}
	sort.Strings(msgs)
	return msgs
}

// loadKandangLeftoverStocks loads every product_warehouse row for KANDANG-type
// warehouses where on_hand_qty > 0, together with the sum of ACTIVE CONSUME
// stock_allocations so callers can compute the leftover qty.
func loadKandangLeftoverStocks(ctx context.Context, db *gorm.DB, opts *commandOptions) ([]kandangStockRow, error) {
	type row struct {
		LocationID          uint    `gorm:"column:location_id"`
		LocationName        string  `gorm:"column:location_name"`
		SourceWarehouseID   uint    `gorm:"column:source_warehouse_id"`
		SourceWarehouseName string  `gorm:"column:source_warehouse_name"`
		ProductWarehouseID  uint    `gorm:"column:product_warehouse_id"`
		ProductID           uint    `gorm:"column:product_id"`
		ProductName         string  `gorm:"column:product_name"`
		OnHandQty           float64 `gorm:"column:on_hand_qty"`
		AllocatedQty        float64 `gorm:"column:allocated_qty"`
	}

	query := db.WithContext(ctx).
		Table("product_warehouses pw").
		Select(`
			kw.location_id   AS location_id,
			l.name           AS location_name,
			kw.id            AS source_warehouse_id,
			kw.name          AS source_warehouse_name,
			pw.id            AS product_warehouse_id,
			pw.product_id    AS product_id,
			p.name           AS product_name,
			COALESCE(pw.qty, 0) AS on_hand_qty,
			COALESCE((
				SELECT SUM(sa.qty)
				FROM stock_allocations sa
				WHERE sa.product_warehouse_id = pw.id
				  AND sa.status = 'ACTIVE'
				  AND sa.allocation_purpose = 'CONSUME'
				  AND sa.deleted_at IS NULL
			), 0) AS allocated_qty
		`).
		Joins("JOIN warehouses kw ON kw.id = pw.warehouse_id AND kw.deleted_at IS NULL").
		Joins("JOIN locations l ON l.id = kw.location_id").
		Joins("JOIN products p ON p.id = pw.product_id AND p.deleted_at IS NULL").
		Where("UPPER(kw.type) = 'KANDANG'").
		Where("COALESCE(pw.qty, 0) > 0").
		Order("l.name ASC, kw.name ASC, p.name ASC")

	query = applyLocationFilter(query, opts, "kw")

	var rows []row
	if err := query.Scan(&rows).Error; err != nil {
		return nil, err
	}

	result := make([]kandangStockRow, 0, len(rows))
	for _, r := range rows {
		result = append(result, kandangStockRow{
			LocationID:          r.LocationID,
			LocationName:        r.LocationName,
			SourceWarehouseID:   r.SourceWarehouseID,
			SourceWarehouseName: r.SourceWarehouseName,
			ProductWarehouseID:  r.ProductWarehouseID,
			ProductID:           r.ProductID,
			ProductName:         r.ProductName,
			OnHandQty:           r.OnHandQty,
			AllocatedQty:        r.AllocatedQty,
			LeftoverQty:         r.OnHandQty - r.AllocatedQty,
		})
	}
	return result, nil
}

// ── Plan building ─────────────────────────────────────────────────────────────

func buildTransferPlan(
	opts *commandOptions,
	farmMap map[uint]farmWarehouseInfo,
	stocks []kandangStockRow,
) ([]transferReportRow, []transferGroup) {
	reportRows := make([]transferReportRow, 0, len(stocks))
	groupMap := make(map[string]*transferGroup) // key: "srcWarehouseID:farmWarehouseID"

	for _, s := range stocks {
		farm := farmMap[s.LocationID]

		report := transferReportRow{
			RunID:               opts.RunID,
			LocationID:          s.LocationID,
			LocationName:        s.LocationName,
			SourceWarehouseID:   s.SourceWarehouseID,
			SourceWarehouseName: s.SourceWarehouseName,
			ProductWarehouseID:  s.ProductWarehouseID,
			ProductID:           s.ProductID,
			ProductName:         s.ProductName,
			OnHandQty:           s.OnHandQty,
			AllocatedQty:        s.AllocatedQty,
			Qty:                 s.LeftoverQty,
			Status:              "eligible",
		}

		switch {
		case farm.FarmCount == 0:
			report.Status = "skipped"
			report.Reason = "missing_farm_warehouse"
		case farm.FarmCount > 1:
			// Treat as a hard error row so the operator knows to fix it.
			report.Status = "error"
			report.Reason = fmt.Sprintf("multiple_farm_warehouses (found %d)", farm.FarmCount)
		case s.LeftoverQty <= 0:
			report.Status = "skipped"
			if s.AllocatedQty > 0 {
				report.Reason = fmt.Sprintf("fully_allocated (on_hand=%.3f allocated=%.3f)", s.OnHandQty, s.AllocatedQty)
			} else {
				report.Reason = "zero_on_hand_qty"
			}
		}

		if farm.FarmCount == 1 {
			fwID := farm.WarehouseID
			fwName := farm.WarehouseName
			report.FarmWarehouseID = &fwID
			report.FarmWarehouseName = &fwName
		}

		reportRows = append(reportRows, report)
		if report.Status != "eligible" {
			continue
		}

		groupKey := fmt.Sprintf("%d:%d", s.SourceWarehouseID, farm.WarehouseID)
		grp := groupMap[groupKey]
		if grp == nil {
			grp = &transferGroup{
				LocationID:          s.LocationID,
				LocationName:        s.LocationName,
				SourceWarehouseID:   s.SourceWarehouseID,
				SourceWarehouseName: s.SourceWarehouseName,
				FarmWarehouseID:     farm.WarehouseID,
				FarmWarehouseName:   farm.WarehouseName,
			}
			groupMap[groupKey] = grp
		}
		grp.Rows = append(grp.Rows, &reportRows[len(reportRows)-1])
	}

	groups := make([]transferGroup, 0, len(groupMap))
	for _, grp := range groupMap {
		sort.Slice(grp.Rows, func(i, j int) bool {
			return grp.Rows[i].ProductName < grp.Rows[j].ProductName
		})
		groups = append(groups, *grp)
	}
	sort.Slice(groups, func(i, j int) bool {
		if groups[i].LocationName == groups[j].LocationName {
			return groups[i].SourceWarehouseName < groups[j].SourceWarehouseName
		}
		return groups[i].LocationName < groups[j].LocationName
	})

	return reportRows, groups
}

// ── Apply / Rollback execution ────────────────────────────────────────────────

func executeApply(
	ctx context.Context,
	svc systemTransferExecutor,
	opts *commandOptions,
	groups []transferGroup,
) (applySummary, error) {
	summary := applySummary{GroupsPlanned: len(groups)}

	for i := range groups {
		grp := &groups[i]
		products := make([]transferSvc.SystemTransferProduct, 0, len(grp.Rows))
		for _, row := range grp.Rows {
			products = append(products, transferSvc.SystemTransferProduct{
				ProductID:  row.ProductID,
				ProductQty: row.Qty,
			})
		}

		reason := buildTransferReason(opts.RunID, grp.LocationName, grp.SourceWarehouseName, grp.FarmWarehouseName, opts.TransferDate)
		notes := buildStockLogNotes(opts.RunID, grp.LocationName, grp.SourceWarehouseName, grp.FarmWarehouseName, opts.TransferDate)

		transfer, err := svc.CreateSystemTransfer(ctx, &transferSvc.SystemTransferRequest{
			TransferReason:         reason,
			TransferDate:           opts.TransferDate,
			SourceWarehouseID:      grp.SourceWarehouseID,
			DestinationWarehouseID: grp.FarmWarehouseID,
			Products:               products,
			ActorID:                opts.ActorID,
			StockLogNotes:          notes,
		})
		if err != nil {
			for _, row := range grp.Rows {
				row.Status = "failed"
				row.Reason = err.Error()
				summary.RowsFailed++
			}
			continue
		}

		summary.GroupsApplied++
		for _, row := range grp.Rows {
			row.Status = "applied"
			row.TransferID = &transfer.Id
			row.MovementNumber = &transfer.MovementNumber
			summary.RowsApplied++
		}
	}

	for _, grp := range groups {
		summary.RowsPlanned += len(grp.Rows)
	}
	return summary, nil
}

func executeRollback(
	ctx context.Context,
	svc systemTransferExecutor,
	rows []rollbackDetailRow,
	actorID uint,
) error {
	if actorID == 0 {
		return fmt.Errorf("--actor-id is required for rollback")
	}

	byTransfer := make(map[uint64][]int)
	for idx, row := range rows {
		byTransfer[row.TransferID] = append(byTransfer[row.TransferID], idx)
	}

	transferIDs := make([]uint64, 0, len(byTransfer))
	for id := range byTransfer {
		transferIDs = append(transferIDs, id)
	}
	// Delete in descending order to minimize downstream conflicts.
	sort.Slice(transferIDs, func(i, j int) bool { return transferIDs[i] > transferIDs[j] })

	var firstErr error
	for _, id := range transferIDs {
		err := svc.DeleteSystemTransfer(ctx, uint(id), actorID)
		for _, idx := range byTransfer[id] {
			if err != nil {
				rows[idx].Status = "failed"
				rows[idx].Reason = err.Error()
			} else {
				rows[idx].Status = "rolled_back"
			}
		}
		if err != nil && firstErr == nil {
			firstErr = err
		}
	}

	return firstErr
}

// loadRollbackDetails finds all stock transfer rows that were created by the
// given run_id (matched via the transfer reason field).
func loadRollbackDetails(ctx context.Context, db *gorm.DB, runID string) ([]rollbackDetailRow, error) {
	type row struct {
		TransferID          uint64  `gorm:"column:transfer_id"`
		MovementNumber      string  `gorm:"column:movement_number"`
		LocationName        string  `gorm:"column:location_name"`
		SourceWarehouseName string  `gorm:"column:source_warehouse_name"`
		FarmWarehouseName   string  `gorm:"column:farm_warehouse_name"`
		ProductName         string  `gorm:"column:product_name"`
		Qty                 float64 `gorm:"column:qty"`
	}

	needle := buildRunReasonMatcher(runID)
	var dbRows []row
	err := db.WithContext(ctx).
		Table("stock_transfers st").
		Select(`
			st.id                                           AS transfer_id,
			st.movement_number                              AS movement_number,
			COALESCE(loc.name, '')                          AS location_name,
			ws.name                                         AS source_warehouse_name,
			wd.name                                         AS farm_warehouse_name,
			p.name                                          AS product_name,
			COALESCE(std.total_qty, std.usage_qty, 0)       AS qty
		`).
		Joins("JOIN warehouses ws  ON ws.id  = st.from_warehouse_id").
		Joins("JOIN warehouses wd  ON wd.id  = st.to_warehouse_id").
		Joins("LEFT JOIN locations loc ON loc.id = COALESCE(ws.location_id, wd.location_id)").
		Joins("JOIN stock_transfer_details std ON std.stock_transfer_id = st.id AND std.deleted_at IS NULL").
		Joins("JOIN products p ON p.id = std.product_id").
		Where("st.deleted_at IS NULL").
		Where("st.reason LIKE ?", needle).
		Order("st.id DESC, std.id ASC").
		Scan(&dbRows).Error
	if err != nil {
		return nil, err
	}

	rows := make([]rollbackDetailRow, 0, len(dbRows))
	for _, r := range dbRows {
		rows = append(rows, rollbackDetailRow{
			RunID:               runID,
			TransferID:          r.TransferID,
			MovementNumber:      r.MovementNumber,
			LocationName:        r.LocationName,
			SourceWarehouseName: r.SourceWarehouseName,
			FarmWarehouseName:   r.FarmWarehouseName,
			ProductName:         r.ProductName,
			Qty:                 r.Qty,
		})
	}
	return rows, nil
}

// ── Helpers ───────────────────────────────────────────────────────────────────

func applyLocationFilter(q *gorm.DB, opts *commandOptions, tableAlias string) *gorm.DB {
	if opts == nil {
		return q
	}
	switch {
	case opts.LocationID > 0:
		return q.Where(tableAlias+".location_id = ?", opts.LocationID)
	case opts.LocationName != "":
		return q.Where("LOWER(l.name) = LOWER(?)", opts.LocationName)
	default:
		return q
	}
}

// buildTransferReason produces a structured string stored in stock_transfers.reason.
// It is used as the rollback lookup key, so must remain stable and parseable.
func buildTransferReason(runID, locationName, srcWarehouse, farmWarehouse string, date time.Time) string {
	return fmt.Sprintf(
		"%s|run_id=%s|location=%s|src_warehouse=%s|farm_warehouse=%s|transfer_date=%s",
		transferReasonPrefix,
		runID,
		sanitizePipeField(locationName),
		sanitizePipeField(srcWarehouse),
		sanitizePipeField(farmWarehouse),
		date.Format("2006-01-02"),
	)
}

// buildStockLogNotes produces a human-readable note attached to each stock log
// entry so operators can trace the origin of stock movements in the logs.
func buildStockLogNotes(runID, locationName, srcWarehouse, farmWarehouse string, date time.Time) string {
	return fmt.Sprintf(
		"[auto] leftover stock transfer from kandang to farm | run_id=%s | location=%s | from=%s | to=%s | date=%s",
		runID,
		locationName,
		srcWarehouse,
		farmWarehouse,
		date.Format("2006-01-02"),
	)
}

// buildRunReasonMatcher returns a LIKE pattern that matches all transfers from
// a specific run_id regardless of the other fields in the reason string.
func buildRunReasonMatcher(runID string) string {
	return fmt.Sprintf("%s|run_id=%s|%%", transferReasonPrefix, strings.TrimSpace(runID))
}

func buildRunID() string {
	return fmt.Sprintf("product-farm-transfer-%s", time.Now().UTC().Format("20060102T150405.000000000Z"))
}

func sanitizePipeField(s string) string {
	return strings.ReplaceAll(strings.TrimSpace(s), "|", "/")
}

func normalizeDateOnly(t time.Time) time.Time {
	return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
}

func derefString(s *string) string {
	if s == nil {
		return ""
	}
	return *s
}

// flattenGroups rebuilds reportRows from groups (which carry applied/failed
// status) then appends skipped/error rows from the original slice.
func flattenGroups(groups []transferGroup, fallback []transferReportRow) []transferReportRow {
	if len(groups) == 0 {
		return fallback
	}
	rows := make([]transferReportRow, 0, len(fallback))
	for _, grp := range groups {
		for _, row := range grp.Rows {
			rows = append(rows, *row)
		}
	}
	for _, row := range fallback {
		if row.Status == "skipped" || row.Status == "error" {
			rows = append(rows, row)
		}
	}
	sort.Slice(rows, func(i, j int) bool {
		if rows[i].LocationName == rows[j].LocationName {
			if rows[i].SourceWarehouseName == rows[j].SourceWarehouseName {
				return rows[i].ProductName < rows[j].ProductName
			}
			return rows[i].SourceWarehouseName < rows[j].SourceWarehouseName
		}
		return rows[i].LocationName < rows[j].LocationName
	})
	return rows
}

func summarizeReport(rows []transferReportRow, groups []transferGroup, appliedGroups int) applySummary {
	summary := applySummary{
		GroupsPlanned: len(groups),
		GroupsApplied: appliedGroups,
	}
	for _, row := range rows {
		switch row.Status {
		case "eligible":
			summary.RowsPlanned++
		case "applied":
			summary.RowsPlanned++
			summary.RowsApplied++
		case "failed":
			summary.RowsPlanned++
			summary.RowsFailed++
		case "skipped":
			summary.RowsSkipped++
		case "error":
			summary.RowsError++
		}
	}
	return summary
}

// ── Rendering ─────────────────────────────────────────────────────────────────

func renderTransferReport(mode string, rows []transferReportRow, summary applySummary) {
	if mode == outputModeJSON {
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		_ = enc.Encode(map[string]any{"rows": rows, "summary": summary})
		return
	}

	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	fmt.Fprintln(w, "RUN_ID\tLOCATION\tSOURCE_WAREHOUSE\tFARM_WAREHOUSE\tPRODUCT\tON_HAND\tALLOCATED\tLEFTOVER\tSTATUS\tREASON\tTRANSFER_ID\tMOVEMENT_NUMBER")
	for _, row := range rows {
		transferID := "-"
		if row.TransferID != nil {
			transferID = fmt.Sprintf("%d", *row.TransferID)
		}
		movementNumber := "-"
		if row.MovementNumber != nil {
			movementNumber = *row.MovementNumber
		}
		fmt.Fprintf(w,
			"%s\t%s\t%s\t%s\t%s\t%.3f\t%.3f\t%.3f\t%s\t%s\t%s\t%s\n",
			row.RunID,
			row.LocationName,
			row.SourceWarehouseName,
			derefString(row.FarmWarehouseName),
			row.ProductName,
			row.OnHandQty,
			row.AllocatedQty,
			row.Qty,
			row.Status,
			row.Reason,
			transferID,
			movementNumber,
		)
	}
	_ = w.Flush()
	fmt.Printf(
		"\nSummary: rows_planned=%d rows_applied=%d rows_skipped=%d rows_error=%d rows_failed=%d groups_planned=%d groups_applied=%d\n",
		summary.RowsPlanned, summary.RowsApplied, summary.RowsSkipped, summary.RowsError, summary.RowsFailed,
		summary.GroupsPlanned, summary.GroupsApplied,
	)
}

func renderRollbackReport(mode string, rows []rollbackDetailRow) {
	if mode == outputModeJSON {
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		_ = enc.Encode(map[string]any{"rows": rows})
		return
	}

	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	fmt.Fprintln(w, "RUN_ID\tTRANSFER_ID\tMOVEMENT_NUMBER\tLOCATION\tSOURCE_WAREHOUSE\tFARM_WAREHOUSE\tPRODUCT\tQTY\tSTATUS\tREASON")
	for _, row := range rows {
		fmt.Fprintf(w,
			"%s\t%d\t%s\t%s\t%s\t%s\t%s\t%.3f\t%s\t%s\n",
			row.RunID,
			row.TransferID,
			row.MovementNumber,
			row.LocationName,
			row.SourceWarehouseName,
			row.FarmWarehouseName,
			row.ProductName,
			row.Qty,
			row.Status,
			row.Reason,
		)
	}
	_ = w.Flush()
}
