package localdb

import (
	"fmt"
	"log/slog"
	"strings"
	"time"

	"gorm.io/gorm"
)

// ComponentFilter for searching with filters
type ComponentFilter struct {
	Category string
	Search   string
	HasPrice bool
}

// ComponentSyncResult contains statistics from component sync
type ComponentSyncResult struct {
	TotalSynced int
	NewCount    int
	UpdateCount int
	Duration    time.Duration
}

// SyncComponents loads components from MariaDB (lot + qt_lot_metadata) into local_components
func (l *LocalDB) SyncComponents(mariaDB *gorm.DB) (*ComponentSyncResult, error) {
	startTime := time.Now()

	// Build the component catalog from every runtime source of LOT names.
	// Storage lots may exist in qt_lot_metadata / qt_pricelist_items before they appear in lot,
	// so the sync cannot start from lot alone.
	type componentRow struct {
		LotName        string
		LotDescription string
		Category       *string
		Model          *string
	}

	var rows []componentRow
	err := mariaDB.Raw(`
		SELECT
			src.lot_name,
			COALESCE(MAX(NULLIF(TRIM(l.lot_description), '')), '') AS lot_description,
			COALESCE(
				MAX(NULLIF(TRIM(c.code), '')),
				MAX(NULLIF(TRIM(l.lot_category), '')),
				SUBSTRING_INDEX(src.lot_name, '_', 1)
			) AS category,
			MAX(NULLIF(TRIM(m.model), '')) AS model
		FROM (
			SELECT lot_name FROM lot
			UNION
			SELECT lot_name FROM qt_lot_metadata
			WHERE is_hidden = FALSE OR is_hidden IS NULL
			UNION
			SELECT lot_name FROM qt_pricelist_items
		) src
		LEFT JOIN lot l ON l.lot_name = src.lot_name
		LEFT JOIN qt_lot_metadata m
			ON m.lot_name = src.lot_name
			AND (m.is_hidden = FALSE OR m.is_hidden IS NULL)
		LEFT JOIN qt_categories c ON m.category_id = c.id
		GROUP BY src.lot_name
		ORDER BY src.lot_name
	`).Scan(&rows).Error
	if err != nil {
		return nil, fmt.Errorf("querying components from MariaDB: %w", err)
	}

	if len(rows) == 0 {
		slog.Warn("no components found in MariaDB")
		return &ComponentSyncResult{
			Duration: time.Since(startTime),
		}, nil
	}

	// Get existing local components for comparison
	existingMap := make(map[string]bool)
	var existing []LocalComponent
	if err := l.db.Find(&existing).Error; err != nil {
		return nil, fmt.Errorf("reading existing local components: %w", err)
	}
	for _, c := range existing {
		existingMap[c.LotName] = true
	}

	// Prepare components for batch insert/update.
	// Source joins may duplicate the same lot_name, so collapse them before insert.
	syncTime := time.Now()
	components := make([]LocalComponent, 0, len(rows))
	componentIndex := make(map[string]int, len(rows))
	newCount := 0

	for _, row := range rows {
		lotName := strings.TrimSpace(row.LotName)
		if lotName == "" {
			continue
		}

		category := ""
		if row.Category != nil {
			category = strings.TrimSpace(*row.Category)
		} else {
			// Parse category from lot_name (e.g., "CPU_AMD_9654" -> "CPU")
			parts := strings.SplitN(lotName, "_", 2)
			if len(parts) >= 1 {
				category = parts[0]
			}
		}

		model := ""
		if row.Model != nil {
			model = strings.TrimSpace(*row.Model)
		}

		comp := LocalComponent{
			LotName:        lotName,
			LotDescription: strings.TrimSpace(row.LotDescription),
			Category:       category,
			Model:          model,
		}

		if idx, exists := componentIndex[lotName]; exists {
			// Keep the first row, but fill any missing metadata from duplicates.
			if components[idx].LotDescription == "" && comp.LotDescription != "" {
				components[idx].LotDescription = comp.LotDescription
			}
			if components[idx].Category == "" && comp.Category != "" {
				components[idx].Category = comp.Category
			}
			if components[idx].Model == "" && comp.Model != "" {
				components[idx].Model = comp.Model
			}
			continue
		}

		componentIndex[lotName] = len(components)
		components = append(components, comp)

		if !existingMap[lotName] {
			newCount++
		}
	}

	// Use transaction for bulk upsert
	err = l.db.Transaction(func(tx *gorm.DB) error {
		// Delete all existing and insert new (simpler than upsert for SQLite)
		if err := tx.Where("1=1").Delete(&LocalComponent{}).Error; err != nil {
			return fmt.Errorf("clearing local components: %w", err)
		}

		// Batch insert
		batchSize := 500
		for i := 0; i < len(components); i += batchSize {
			end := i + batchSize
			if end > len(components) {
				end = len(components)
			}
			if err := tx.CreateInBatches(components[i:end], batchSize).Error; err != nil {
				return fmt.Errorf("inserting components batch: %w", err)
			}
		}

		return nil
	})
	if err != nil {
		return nil, err
	}

	// Update last sync time
	if err := l.SetComponentSyncTime(syncTime); err != nil {
		slog.Warn("failed to update component sync time", "error", err)
	}

	result := &ComponentSyncResult{
		TotalSynced: len(components),
		NewCount:    newCount,
		UpdateCount: len(components) - newCount,
		Duration:    time.Since(startTime),
	}

	slog.Info("components synced",
		"total", result.TotalSynced,
		"new", result.NewCount,
		"updated", result.UpdateCount,
		"duration", result.Duration)

	return result, nil
}

// SearchLocalComponents searches components in local cache by query string
// Searches in lot_name, lot_description, category, and model fields
func (l *LocalDB) SearchLocalComponents(query string, limit int) ([]LocalComponent, error) {
	if limit <= 0 {
		limit = 50
	}

	var components []LocalComponent

	if query == "" {
		// Return all components with limit
		err := l.db.Order("lot_name").Limit(limit).Find(&components).Error
		return components, err
	}

	// Search with LIKE on multiple fields
	searchPattern := "%" + strings.ToLower(query) + "%"

	err := l.db.Where(
		"LOWER(lot_name) LIKE ? OR LOWER(lot_description) LIKE ? OR LOWER(category) LIKE ? OR LOWER(model) LIKE ?",
		searchPattern, searchPattern, searchPattern, searchPattern,
	).Order("lot_name").Limit(limit).Find(&components).Error

	return components, err
}

// SearchLocalComponentsByCategory searches components by category and optional query
func (l *LocalDB) SearchLocalComponentsByCategory(category string, query string, limit int) ([]LocalComponent, error) {
	if limit <= 0 {
		limit = 50
	}

	var components []LocalComponent
	db := l.db.Where("LOWER(category) = ?", strings.ToLower(category))

	if query != "" {
		searchPattern := "%" + strings.ToLower(query) + "%"
		db = db.Where(
			"LOWER(lot_name) LIKE ? OR LOWER(lot_description) LIKE ? OR LOWER(model) LIKE ?",
			searchPattern, searchPattern, searchPattern,
		)
	}

	err := db.Order("lot_name").Limit(limit).Find(&components).Error
	return components, err
}

// ListComponents returns components with filtering and pagination
func (l *LocalDB) ListComponents(filter ComponentFilter, offset, limit int) ([]LocalComponent, int64, error) {
	db := l.db

	// Apply category filter
	if filter.Category != "" {
		db = db.Where("LOWER(category) = ?", strings.ToLower(filter.Category))
	}

	// Apply search filter
	if filter.Search != "" {
		searchPattern := "%" + strings.ToLower(filter.Search) + "%"
		db = db.Where(
			"LOWER(lot_name) LIKE ? OR LOWER(lot_description) LIKE ? OR LOWER(category) LIKE ? OR LOWER(model) LIKE ?",
			searchPattern, searchPattern, searchPattern, searchPattern,
		)
	}

	// Get total count
	var total int64
	if err := db.Model(&LocalComponent{}).Count(&total).Error; err != nil {
		return nil, 0, err
	}

	// Apply pagination and get results
	var components []LocalComponent
	if err := db.Order("lot_name").Offset(offset).Limit(limit).Find(&components).Error; err != nil {
		return nil, 0, err
	}

	return components, total, nil
}

// GetLocalComponent returns a single component by lot_name
func (l *LocalDB) GetLocalComponent(lotName string) (*LocalComponent, error) {
	var component LocalComponent
	err := l.db.Where("lot_name = ?", lotName).First(&component).Error
	if err != nil {
		return nil, err
	}
	return &component, nil
}

// GetLocalComponentCategoriesByLotNames returns category for each lot_name in the local component cache.
// Missing lots are not included in the map; caller is responsible for strict validation.
func (l *LocalDB) GetLocalComponentCategoriesByLotNames(lotNames []string) (map[string]string, error) {
	result := make(map[string]string, len(lotNames))
	if len(lotNames) == 0 {
		return result, nil
	}

	type row struct {
		LotName  string `gorm:"column:lot_name"`
		Category string `gorm:"column:category"`
	}
	var rows []row
	if err := l.db.Model(&LocalComponent{}).
		Select("lot_name, category").
		Where("lot_name IN ?", lotNames).
		Find(&rows).Error; err != nil {
		return nil, err
	}
	for _, r := range rows {
		result[r.LotName] = r.Category
	}
	return result, nil
}

// GetLocalComponentCategories returns distinct categories from local components
func (l *LocalDB) GetLocalComponentCategories() ([]string, error) {
	var categories []string
	err := l.db.Model(&LocalComponent{}).
		Distinct("category").
		Where("category != ''").
		Order("category").
		Pluck("category", &categories).Error
	return categories, err
}

// CountLocalComponents returns the total number of local components
func (l *LocalDB) CountLocalComponents() int64 {
	var count int64
	l.db.Model(&LocalComponent{}).Count(&count)
	return count
}

// CountLocalComponentsByCategory returns component count by category
func (l *LocalDB) CountLocalComponentsByCategory(category string) int64 {
	var count int64
	l.db.Model(&LocalComponent{}).Where("LOWER(category) = ?", strings.ToLower(category)).Count(&count)
	return count
}

// GetComponentSyncTime returns the last component sync timestamp
func (l *LocalDB) GetComponentSyncTime() *time.Time {
	var setting struct {
		Value string
	}
	if err := l.db.Table("app_settings").
		Where("key = ?", "last_component_sync").
		First(&setting).Error; err != nil {
		return nil
	}

	t, err := time.Parse(time.RFC3339, setting.Value)
	if err != nil {
		return nil
	}
	return &t
}

// SetComponentSyncTime sets the last component sync timestamp
func (l *LocalDB) SetComponentSyncTime(t time.Time) error {
	return l.db.Exec(`
		INSERT INTO app_settings (key, value, updated_at)
		VALUES (?, ?, ?)
		ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
	`, "last_component_sync", t.Format(time.RFC3339), time.Now().Format(time.RFC3339)).Error
}

// NeedComponentSync checks if component sync is needed (older than specified hours)
func (l *LocalDB) NeedComponentSync(maxAgeHours int) bool {
	syncTime := l.GetComponentSyncTime()
	if syncTime == nil {
		return true
	}
	return time.Since(*syncTime).Hours() > float64(maxAgeHours)
}
