package fifo

import (
	"errors"
	"fmt"
	"strings"
	"sync"

	"gorm.io/gorm"
)

// QueryScope allows callers to inject custom query modifiers (preloads, filters, etc).
type QueryScope func(*gorm.DB) *gorm.DB

type StockableKey string
type UsableKey string

func (k StockableKey) String() string {
	return string(k)
}

func (k UsableKey) String() string {
	return string(k)
}

// StockableColumns describes the minimum columns required for a stock-bearing row.
type StockableColumns struct {
	ID                 string
	ProductWarehouseID string
	TotalQuantity      string
	TotalUsedQuantity  string
	CreatedAt          string
}

// UsableColumns describes the required columns for rows that consume stock.
type UsableColumns struct {
	ID                 string
	ProductWarehouseID string
	UsageQuantity      string
	PendingQuantity    string
	CreatedAt          string
}

// StockableConfig registers a table that introduces stock into the system (purchases, transfers, etc).
type StockableConfig struct {
	Key     StockableKey
	Table   string
	Columns StockableColumns
	// OrderBy accepts raw column expressions, evaluated in-order (e.g. []string{"created_at ASC", "id ASC"}).
	OrderBy []string
	// Scope lets a module append base filters (e.g. exclude drafts).
	Scope QueryScope
}

// UsableConfig registers a table that consumes stock (recordings, adjustments, sales, etc).
type UsableConfig struct {
	Key                UsableKey
	Table              string
	Columns            UsableColumns
	OrderBy            []string
	Scope              QueryScope
	ExcludedStockables []StockableKey // Stockables to exclude when consuming stock
}

var (
	stockableRegistry = make(map[StockableKey]StockableConfig)
	usableRegistry    = make(map[UsableKey]UsableConfig)
	registryMu        sync.RWMutex
)

// RegisterStockable stores the configuration so services can perform FIFO operations generically.
func RegisterStockable(cfg StockableConfig) error {
	if err := validateStockableConfig(cfg); err != nil {
		return err
	}

	registryMu.Lock()
	defer registryMu.Unlock()

	key := StockableKey(strings.TrimSpace(cfg.Key.String()))
	if _, exists := stockableRegistry[key]; exists {
		return fmt.Errorf("stockable key %q already registered", key)
	}

	stockableRegistry[key] = cfg
	return nil
}

// RegisterUsable stores the configuration for stock-consuming tables.
func RegisterUsable(cfg UsableConfig) error {
	if err := validateUsableConfig(cfg); err != nil {
		return err
	}

	registryMu.Lock()
	defer registryMu.Unlock()

	key := UsableKey(strings.TrimSpace(cfg.Key.String()))
	if _, exists := usableRegistry[key]; exists {
		return fmt.Errorf("usable key %q already registered", key)
	}

	usableRegistry[key] = cfg
	return nil
}

// Stockable returns the registered configuration for the key (if any).
func Stockable(key StockableKey) (StockableConfig, bool) {
	registryMu.RLock()
	defer registryMu.RUnlock()

	cfg, ok := stockableRegistry[key]
	return cfg, ok
}

// Usable returns the registered configuration for the key (if any).
func Usable(key UsableKey) (UsableConfig, bool) {
	registryMu.RLock()
	defer registryMu.RUnlock()

	cfg, ok := usableRegistry[key]
	return cfg, ok
}

// Stockables exposes a copy of the current registry (useful for iterating pending requests).
func Stockables() map[StockableKey]StockableConfig {
	registryMu.RLock()
	defer registryMu.RUnlock()

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

	result := make(map[StockableKey]StockableConfig, len(stockableRegistry))
	for key, cfg := range stockableRegistry {
		result[key] = cfg
	}
	return result
}

// Usables exposes a copy of the usable registry.
func Usables() map[UsableKey]UsableConfig {
	registryMu.RLock()
	defer registryMu.RUnlock()

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

	result := make(map[UsableKey]UsableConfig, len(usableRegistry))
	for key, cfg := range usableRegistry {
		result[key] = cfg
	}
	return result
}

func validateStockableConfig(cfg StockableConfig) error {
	if strings.TrimSpace(cfg.Key.String()) == "" {
		return errors.New("stockable key is required")
	}
	if strings.TrimSpace(cfg.Table) == "" {
		return fmt.Errorf("table name is required for stockable %q", cfg.Key)
	}

	cols := cfg.Columns
	switch {
	case strings.TrimSpace(cols.ID) == "":
		return fmt.Errorf("column id is required for stockable %q", cfg.Key)
	case strings.TrimSpace(cols.ProductWarehouseID) == "":
		return fmt.Errorf("column product warehouse id is required for stockable %q", cfg.Key)
	case strings.TrimSpace(cols.TotalQuantity) == "":
		return fmt.Errorf("column total quantity is required for stockable %q", cfg.Key)
	case strings.TrimSpace(cols.TotalUsedQuantity) == "":
		return fmt.Errorf("column total used quantity is required for stockable %q", cfg.Key)
	case strings.TrimSpace(cols.CreatedAt) == "":
		return fmt.Errorf("column created_at is required for stockable %q", cfg.Key)
	}

	return nil
}

func validateUsableConfig(cfg UsableConfig) error {
	if strings.TrimSpace(cfg.Key.String()) == "" {
		return errors.New("usable key is required")
	}
	if strings.TrimSpace(cfg.Table) == "" {
		return fmt.Errorf("table name is required for usable %q", cfg.Key)
	}

	cols := cfg.Columns
	switch {
	case strings.TrimSpace(cols.ID) == "":
		return fmt.Errorf("column id is required for usable %q", cfg.Key)
	case strings.TrimSpace(cols.ProductWarehouseID) == "":
		return fmt.Errorf("column product warehouse id is required for usable %q", cfg.Key)
	case strings.TrimSpace(cols.UsageQuantity) == "":
		return fmt.Errorf("column usage quantity is required for usable %q", cfg.Key)
	case strings.TrimSpace(cols.PendingQuantity) == "":
		return fmt.Errorf("column pending quantity is required for usable %q", cfg.Key)
	case strings.TrimSpace(cols.CreatedAt) == "":
		return fmt.Errorf("column created_at is required for usable %q", cfg.Key)
	}

	return nil
}
