package sync

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"time"

	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	"git.mchus.pro/mchus/quoteforge/internal/repository"
	"gorm.io/gorm"
)

// PullPartnumberBooks synchronizes partnumber book snapshots from MariaDB to local SQLite.
// Append-only for headers; re-pulls items if a book header exists but has 0 items.
func (s *Service) PullPartnumberBooks() (int, error) {
	slog.Info("starting partnumber book pull")

	mariaDB, err := s.getDB()
	if err != nil {
		return 0, fmt.Errorf("database not available: %w", err)
	}

	localBookRepo := repository.NewPartnumberBookRepository(s.localDB.DB())

	type serverBook struct {
		ID              int       `gorm:"column:id"`
		Version         string    `gorm:"column:version"`
		CreatedAt       time.Time `gorm:"column:created_at"`
		IsActive        bool      `gorm:"column:is_active"`
		PartnumbersJSON string    `gorm:"column:partnumbers_json"`
	}
	var serverBooks []serverBook
	if err := mariaDB.Raw("SELECT id, version, created_at, is_active, partnumbers_json FROM qt_partnumber_books ORDER BY created_at DESC, id DESC").Scan(&serverBooks).Error; err != nil {
		return 0, fmt.Errorf("querying server partnumber books: %w", err)
	}
	slog.Info("partnumber books found on server", "count", len(serverBooks))

	pulled := 0
	for _, sb := range serverBooks {
		var existing localdb.LocalPartnumberBook
		err := s.localDB.DB().Where("server_id = ?", sb.ID).First(&existing).Error
		partnumbers, errPartnumbers := decodeServerPartnumbers(sb.PartnumbersJSON)
		if errPartnumbers != nil {
			slog.Error("failed to decode server partnumbers_json", "server_id", sb.ID, "error", errPartnumbers)
			continue
		}
		if err == nil {
			existing.Version = sb.Version
			existing.CreatedAt = sb.CreatedAt
			existing.IsActive = sb.IsActive
			existing.PartnumbersJSON = partnumbers
			if err := localBookRepo.SaveBook(&existing); err != nil {
				slog.Error("failed to update local partnumber book header", "server_id", sb.ID, "error", err)
				continue
			}

			localItemCount := localBookRepo.CountBookItems(existing.ID)
			if localItemCount > 0 && localBookRepo.HasAllBookItems(existing.ID) {
				slog.Debug("partnumber book already synced, skipping", "server_id", sb.ID, "version", sb.Version, "items", localItemCount)
				continue
			}
			slog.Info("partnumber book header exists but catalog items are missing, re-pulling items", "server_id", sb.ID, "version", sb.Version)
			n, err := pullBookItems(mariaDB, localBookRepo, existing.PartnumbersJSON)
			if err != nil {
				slog.Error("failed to re-pull items for existing book", "server_id", sb.ID, "error", err)
			} else {
				slog.Info("re-pulled items for existing book", "server_id", sb.ID, "version", sb.Version, "items_saved", n)
				pulled++
			}
			continue
		}

		slog.Info("pulling new partnumber book", "server_id", sb.ID, "version", sb.Version, "is_active", sb.IsActive)

		localBook := &localdb.LocalPartnumberBook{
			ServerID:        sb.ID,
			Version:         sb.Version,
			CreatedAt:       sb.CreatedAt,
			IsActive:        sb.IsActive,
			PartnumbersJSON: partnumbers,
		}
		if err := localBookRepo.SaveBook(localBook); err != nil {
			slog.Error("failed to save local partnumber book", "server_id", sb.ID, "error", err)
			continue
		}

		n, err := pullBookItems(mariaDB, localBookRepo, localBook.PartnumbersJSON)
		if err != nil {
			slog.Error("failed to pull items for new book", "server_id", sb.ID, "error", err)
			continue
		}

		slog.Info("partnumber book saved locally", "server_id", sb.ID, "version", sb.Version, "items_saved", n)
		pulled++
	}

	slog.Info("partnumber book pull completed", "new_books_pulled", pulled, "total_on_server", len(serverBooks))
	return pulled, nil
}

// pullBookItems fetches catalog items for a partnumber list from MariaDB and saves them to SQLite.
// Returns the number of items saved.
func pullBookItems(mariaDB *gorm.DB, repo *repository.PartnumberBookRepository, partnumbers localdb.LocalStringList) (int, error) {
	if len(partnumbers) == 0 {
		return 0, nil
	}

	type serverItem struct {
		Partnumber  string `gorm:"column:partnumber"`
		LotsJSON    string `gorm:"column:lots_json"`
		Description string `gorm:"column:description"`
	}
	var serverItems []serverItem
	err := mariaDB.Raw("SELECT partnumber, lots_json, description FROM qt_partnumber_book_items WHERE partnumber IN ?", []string(partnumbers)).Scan(&serverItems).Error
	if err != nil {
		return 0, fmt.Errorf("querying items from server: %w", err)
	}
	slog.Info("partnumber book items fetched from server", "count", len(serverItems), "requested_partnumbers", len(partnumbers))

	if len(serverItems) == 0 {
		slog.Warn("server returned 0 partnumber book items")
		return 0, nil
	}

	localItems := make([]localdb.LocalPartnumberBookItem, 0, len(serverItems))
	for _, si := range serverItems {
		var lots localdb.LocalPartnumberBookLots
		if err := json.Unmarshal([]byte(si.LotsJSON), &lots); err != nil {
			return 0, fmt.Errorf("decode lots_json for %s: %w", si.Partnumber, err)
		}
		localItems = append(localItems, localdb.LocalPartnumberBookItem{
			Partnumber:  si.Partnumber,
			LotsJSON:    lots,
			Description: si.Description,
		})
	}
	if err := repo.SaveBookItems(localItems); err != nil {
		return 0, fmt.Errorf("saving items to local db: %w", err)
	}
	return len(localItems), nil
}

func decodeServerPartnumbers(raw string) (localdb.LocalStringList, error) {
	if raw == "" {
		return localdb.LocalStringList{}, nil
	}
	var items []string
	if err := json.Unmarshal([]byte(raw), &items); err != nil {
		return nil, err
	}
	return localdb.LocalStringList(items), nil
}
