package handlers

import (
	"errors"
	"fmt"
	"html/template"
	"log/slog"
	"net/http"
	"strings"
	stdsync "sync"
	"time"

	qfassets "git.mchus.pro/mchus/quoteforge"
	"git.mchus.pro/mchus/quoteforge/internal/db"
	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	"git.mchus.pro/mchus/quoteforge/internal/services/sync"
	"github.com/gin-gonic/gin"
)

// SyncHandler handles sync API endpoints
type SyncHandler struct {
	localDB           *localdb.LocalDB
	syncService       *sync.Service
	connMgr           *db.ConnectionManager
	autoSyncInterval  time.Duration
	onlineGraceFactor float64
	tmpl              *template.Template
	readinessMu       stdsync.Mutex
	readinessCached   *sync.SyncReadiness
	readinessCachedAt time.Time
}

// NewSyncHandler creates a new sync handler
func NewSyncHandler(localDB *localdb.LocalDB, syncService *sync.Service, connMgr *db.ConnectionManager, _ string, autoSyncInterval time.Duration) (*SyncHandler, error) {
	// Load sync_status partial template
	tmpl, err := template.ParseFS(qfassets.TemplatesFS, "web/templates/partials/sync_status.html")
	if err != nil {
		return nil, err
	}

	return &SyncHandler{
		localDB:           localDB,
		syncService:       syncService,
		connMgr:           connMgr,
		autoSyncInterval:  autoSyncInterval,
		onlineGraceFactor: 1.10,
		tmpl:              tmpl,
	}, nil
}

// SyncStatusResponse represents the sync status
type SyncStatusResponse struct {
	LastComponentSync       *time.Time          `json:"last_component_sync"`
	LastPricelistSync       *time.Time          `json:"last_pricelist_sync"`
	LastPricelistAttemptAt  *time.Time          `json:"last_pricelist_attempt_at,omitempty"`
	LastPricelistSyncStatus string              `json:"last_pricelist_sync_status,omitempty"`
	LastPricelistSyncError  string              `json:"last_pricelist_sync_error,omitempty"`
	HasIncompleteServerSync bool                `json:"has_incomplete_server_sync"`
	KnownServerChangesMiss  bool                `json:"known_server_changes_missing"`
	IsOnline                bool                `json:"is_online"`
	ComponentsCount         int64               `json:"components_count"`
	PricelistsCount         int64               `json:"pricelists_count"`
	ServerPricelists        int                 `json:"server_pricelists"`
	NeedComponentSync       bool                `json:"need_component_sync"`
	NeedPricelistSync       bool                `json:"need_pricelist_sync"`
	Readiness               *sync.SyncReadiness `json:"readiness,omitempty"`
}

type SyncReadinessResponse struct {
	Status                string     `json:"status"`
	Blocked               bool       `json:"blocked"`
	ReasonCode            string     `json:"reason_code,omitempty"`
	ReasonText            string     `json:"reason_text,omitempty"`
	RequiredMinAppVersion *string    `json:"required_min_app_version,omitempty"`
	LastCheckedAt         *time.Time `json:"last_checked_at,omitempty"`
}

// GetStatus returns current sync status
// GET /api/sync/status
func (h *SyncHandler) GetStatus(c *gin.Context) {
	connStatus := h.connMgr.GetStatus()
	isOnline := connStatus.IsConnected && strings.TrimSpace(connStatus.LastError) == ""
	lastComponentSync := h.localDB.GetComponentSyncTime()
	lastPricelistSync := h.localDB.GetLastSyncTime()
	componentsCount := h.localDB.CountLocalComponents()
	pricelistsCount := h.localDB.CountLocalPricelists()
	lastPricelistAttemptAt := h.localDB.GetLastPricelistSyncAttemptAt()
	lastPricelistSyncStatus := h.localDB.GetLastPricelistSyncStatus()
	lastPricelistSyncError := h.localDB.GetLastPricelistSyncError()
	hasFailedSync := strings.EqualFold(lastPricelistSyncStatus, "failed")
	needComponentSync := h.localDB.NeedComponentSync(24)
	readiness := h.getReadinessLocal()

	c.JSON(http.StatusOK, SyncStatusResponse{
		LastComponentSync:       lastComponentSync,
		LastPricelistSync:       lastPricelistSync,
		LastPricelistAttemptAt:  lastPricelistAttemptAt,
		LastPricelistSyncStatus: lastPricelistSyncStatus,
		LastPricelistSyncError:  lastPricelistSyncError,
		HasIncompleteServerSync: hasFailedSync,
		KnownServerChangesMiss:  hasFailedSync,
		IsOnline:                isOnline,
		ComponentsCount:         componentsCount,
		PricelistsCount:         pricelistsCount,
		ServerPricelists:        0,
		NeedComponentSync:       needComponentSync,
		NeedPricelistSync:       lastPricelistSync == nil || hasFailedSync,
		Readiness:               readiness,
	})
}

// GetReadiness returns sync readiness guard status.
// GET /api/sync/readiness
func (h *SyncHandler) GetReadiness(c *gin.Context) {
	readiness, err := h.syncService.GetReadiness()
	if err != nil && readiness == nil {
		RespondError(c, http.StatusInternalServerError, "internal server error", err)
		return
	}
	if readiness == nil {
		c.JSON(http.StatusOK, SyncReadinessResponse{Status: sync.ReadinessUnknown, Blocked: false})
		return
	}
	c.JSON(http.StatusOK, SyncReadinessResponse{
		Status:                readiness.Status,
		Blocked:               readiness.Blocked,
		ReasonCode:            readiness.ReasonCode,
		ReasonText:            readiness.ReasonText,
		RequiredMinAppVersion: readiness.RequiredMinAppVersion,
		LastCheckedAt:         readiness.LastCheckedAt,
	})
}

func (h *SyncHandler) ensureSyncReadiness(c *gin.Context) bool {
	readiness, err := h.syncService.EnsureReadinessForSync()
	if err == nil {
		return true
	}

	blocked := &sync.SyncBlockedError{}
	if errors.As(err, &blocked) {
		c.JSON(http.StatusLocked, gin.H{
			"success":                  false,
			"error":                    blocked.Error(),
			"reason_code":              blocked.Readiness.ReasonCode,
			"reason_text":              blocked.Readiness.ReasonText,
			"required_min_app_version": blocked.Readiness.RequiredMinAppVersion,
			"status":                   blocked.Readiness.Status,
			"blocked":                  true,
			"last_checked_at":          blocked.Readiness.LastCheckedAt,
		})
		return false
	}

	c.JSON(http.StatusInternalServerError, gin.H{
		"success": false,
		"error":   "internal server error",
	})
	_ = c.Error(err)
	_ = readiness
	return false
}

// SyncResultResponse represents sync operation result
type SyncResultResponse struct {
	Success  bool   `json:"success"`
	Message  string `json:"message"`
	Synced   int    `json:"synced"`
	Duration string `json:"duration"`
}

// SyncComponents syncs components from MariaDB to local SQLite
// POST /api/sync/components
func (h *SyncHandler) SyncComponents(c *gin.Context) {
	if !h.ensureSyncReadiness(c) {
		return
	}

	// Get database connection from ConnectionManager
	mariaDB, err := h.connMgr.GetDB()
	if err != nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{
			"success": false,
			"error":   "database connection failed",
		})
		_ = c.Error(err)
		return
	}

	now := time.Now()
	result, err := h.localDB.SyncComponents(mariaDB)
	if err != nil {
		_ = h.localDB.SetComponentSyncResult("error", err.Error(), now)
		h.localDB.AppendSyncLog("components", "error", err.Error(), 0, now, time.Since(now).Milliseconds())
		slog.Error("component sync failed", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"success": false,
			"error":   "component sync failed",
		})
		_ = c.Error(err)
		return
	}
	_ = h.localDB.SetComponentSyncResult("ok", "", now)
	h.localDB.AppendSyncLog("components", "ok", "", result.TotalSynced, now, result.Duration.Milliseconds())

	if err := h.localDB.SyncQtSettings(mariaDB); err != nil {
		slog.Warn("qt_settings sync failed", "error", err)
	}

	c.JSON(http.StatusOK, SyncResultResponse{
		Success:  true,
		Message:  "Components synced successfully",
		Synced:   result.TotalSynced,
		Duration: result.Duration.String(),
	})
}

// SyncPricelists syncs pricelists from MariaDB to local SQLite
// POST /api/sync/pricelists
func (h *SyncHandler) SyncPricelists(c *gin.Context) {
	if !h.ensureSyncReadiness(c) {
		return
	}

	startTime := time.Now()
	synced, err := h.syncService.SyncPricelists()
	if err != nil {
		h.localDB.AppendSyncLog("pricelists", "error", err.Error(), 0, startTime, time.Since(startTime).Milliseconds())
		slog.Error("pricelist sync failed", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"success": false,
			"error":   "pricelist sync failed",
		})
		_ = c.Error(err)
		return
	}
	h.localDB.AppendSyncLog("pricelists", "ok", "", synced, startTime, time.Since(startTime).Milliseconds())

	if _, err := h.syncService.PullPartnumberBooks(); err != nil {
		slog.Warn("partnumber books pull failed after pricelist sync", "error", err)
	}

	c.JSON(http.StatusOK, SyncResultResponse{
		Success:  true,
		Message:  "Pricelists synced successfully",
		Synced:   synced,
		Duration: time.Since(startTime).String(),
	})
}

// SyncPartnumberBooks syncs partnumber book snapshots from MariaDB to local SQLite.
// POST /api/sync/partnumber-books
func (h *SyncHandler) SyncPartnumberBooks(c *gin.Context) {
	if !h.ensureSyncReadiness(c) {
		return
	}

	startTime := time.Now()
	pulled, err := h.syncService.PullPartnumberBooks()
	if err != nil {
		slog.Error("partnumber books pull failed", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"success": false,
			"error":   "partnumber books sync failed",
		})
		_ = c.Error(err)
		return
	}

	c.JSON(http.StatusOK, SyncResultResponse{
		Success:  true,
		Message:  "Partnumber books synced successfully",
		Synced:   pulled,
		Duration: time.Since(startTime).String(),
	})
}

// SyncAllResponse represents result of full sync
type SyncAllResponse struct {
	Success                bool   `json:"success"`
	Message                string `json:"message"`
	PendingPushed          int    `json:"pending_pushed"`
	ComponentsSynced       int    `json:"components_synced"`
	PricelistsSynced       int    `json:"pricelists_synced"`
	ProjectsImported       int    `json:"projects_imported"`
	ProjectsUpdated        int    `json:"projects_updated"`
	ProjectsSkipped        int    `json:"projects_skipped"`
	ConfigurationsImported int    `json:"configurations_imported"`
	ConfigurationsUpdated  int    `json:"configurations_updated"`
	ConfigurationsSkipped  int    `json:"configurations_skipped"`
	Duration               string `json:"duration"`
}

// SyncAll performs full bidirectional sync:
// - push pending local changes (projects/configurations) to server
// - pull components, pricelists, projects, and configurations from server
// POST /api/sync/all
func (h *SyncHandler) SyncAll(c *gin.Context) {
	if !h.ensureSyncReadiness(c) {
		return
	}

	startTime := time.Now()
	var pendingPushed, componentsSynced, pricelistsSynced int

	// Push local pending changes first (projects/configurations)
	pendingPushed, err := h.syncService.PushPendingChanges()
	if err != nil {
		slog.Error("pending push failed during full sync", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"success": false,
			"error":   "pending changes push failed",
		})
		_ = c.Error(err)
		return
	}

	// Sync components
	mariaDB, err := h.connMgr.GetDB()
	if err != nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{
			"success": false,
			"error":   "database connection failed",
		})
		_ = c.Error(err)
		return
	}

	compNow := time.Now()
	compResult, err := h.localDB.SyncComponents(mariaDB)
	if err != nil {
		_ = h.localDB.SetComponentSyncResult("error", err.Error(), compNow)
		h.localDB.AppendSyncLog("components", "error", err.Error(), 0, compNow, time.Since(compNow).Milliseconds())
		slog.Error("component sync failed during full sync", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"success": false,
			"error":   "component sync failed",
		})
		_ = c.Error(err)
		return
	}
	_ = h.localDB.SetComponentSyncResult("ok", "", compNow)
	h.localDB.AppendSyncLog("components", "ok", "", compResult.TotalSynced, compNow, compResult.Duration.Milliseconds())
	componentsSynced = compResult.TotalSynced

	if err := h.localDB.SyncQtSettings(mariaDB); err != nil {
		slog.Warn("qt_settings sync failed", "error", err)
	}

	// Sync pricelists
	plNow := time.Now()
	pricelistsSynced, err = h.syncService.SyncPricelists()
	if err != nil {
		h.localDB.AppendSyncLog("pricelists", "error", err.Error(), 0, plNow, time.Since(plNow).Milliseconds())
		slog.Error("pricelist sync failed during full sync", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"success":           false,
			"error":             "pricelist sync failed",
			"pending_pushed":    pendingPushed,
			"components_synced": componentsSynced,
		})
		_ = c.Error(err)
		return
	}
	h.localDB.AppendSyncLog("pricelists", "ok", "", pricelistsSynced, plNow, time.Since(plNow).Milliseconds())

	if _, err := h.syncService.PullPartnumberBooks(); err != nil {
		slog.Warn("partnumber books pull failed during full sync", "error", err)
	}

	projectsResult, err := h.syncService.ImportProjectsToLocal()
	if err != nil {
		slog.Error("project import failed during full sync", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"success":           false,
			"error":             "project import failed",
			"pending_pushed":    pendingPushed,
			"components_synced": componentsSynced,
			"pricelists_synced": pricelistsSynced,
		})
		_ = c.Error(err)
		return
	}

	configsResult, err := h.syncService.ImportConfigurationsToLocal()
	if err != nil {
		slog.Error("configuration import failed during full sync", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"success":           false,
			"error":             "configuration import failed",
			"pending_pushed":    pendingPushed,
			"components_synced": componentsSynced,
			"pricelists_synced": pricelistsSynced,
			"projects_imported": projectsResult.Imported,
			"projects_updated":  projectsResult.Updated,
			"projects_skipped":  projectsResult.Skipped,
		})
		_ = c.Error(err)
		return
	}

	c.JSON(http.StatusOK, SyncAllResponse{
		Success:                true,
		Message:                "Full sync completed successfully",
		PendingPushed:          pendingPushed,
		ComponentsSynced:       componentsSynced,
		PricelistsSynced:       pricelistsSynced,
		ProjectsImported:       projectsResult.Imported,
		ProjectsUpdated:        projectsResult.Updated,
		ProjectsSkipped:        projectsResult.Skipped,
		ConfigurationsImported: configsResult.Imported,
		ConfigurationsUpdated:  configsResult.Updated,
		ConfigurationsSkipped:  configsResult.Skipped,
		Duration:               time.Since(startTime).String(),
	})
}

// checkOnline checks if MariaDB is accessible
func (h *SyncHandler) checkOnline() bool {
	return h.connMgr.IsOnline()
}

// PushPendingChanges pushes all pending changes to the server
// POST /api/sync/push
func (h *SyncHandler) PushPendingChanges(c *gin.Context) {
	if !h.ensureSyncReadiness(c) {
		return
	}

	startTime := time.Now()
	pushed, err := h.syncService.PushPendingChanges()
	if err != nil {
		slog.Error("push pending changes failed", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"success": false,
			"error":   "pending changes push failed",
		})
		_ = c.Error(err)
		return
	}

	c.JSON(http.StatusOK, SyncResultResponse{
		Success:  true,
		Message:  "Pending changes pushed successfully",
		Synced:   pushed,
		Duration: time.Since(startTime).String(),
	})
}

// GetPendingCount returns the number of pending changes
// GET /api/sync/pending/count
func (h *SyncHandler) GetPendingCount(c *gin.Context) {
	count := h.localDB.GetPendingCount()
	c.JSON(http.StatusOK, gin.H{
		"count": count,
	})
}

// GetPendingChanges returns all pending changes
// GET /api/sync/pending
func (h *SyncHandler) GetPendingChanges(c *gin.Context) {
	changes, err := h.localDB.GetPendingChanges()
	if err != nil {
		RespondError(c, http.StatusInternalServerError, "internal server error", err)
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"changes": changes,
	})
}

// RepairPendingChanges attempts to repair errored pending changes
// POST /api/sync/repair
func (h *SyncHandler) RepairPendingChanges(c *gin.Context) {
	repaired, remainingErrors, err := h.localDB.RepairPendingChanges()
	if err != nil {
		slog.Error("repair pending changes failed", "error", err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"success": false,
			"error":   "pending changes repair failed",
		})
		_ = c.Error(err)
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"success":          true,
		"repaired":         repaired,
		"remaining_errors": remainingErrors,
	})
}

// SyncInfoResponse represents sync information for the modal
type SyncInfoResponse struct {
	// Connection
	DBHost string `json:"db_host"`
	DBUser string `json:"db_user"`
	DBName string `json:"db_name"`

	// Status
	IsOnline                bool       `json:"is_online"`
	LastSyncAt              *time.Time `json:"last_sync_at"`
	LastPricelistAttemptAt  *time.Time `json:"last_pricelist_attempt_at,omitempty"`
	LastPricelistSyncStatus string     `json:"last_pricelist_sync_status,omitempty"`
	LastPricelistSyncError  string     `json:"last_pricelist_sync_error,omitempty"`
	NeedPricelistSync       bool       `json:"need_pricelist_sync"`
	HasIncompleteServerSync bool       `json:"has_incomplete_server_sync"`

	// Statistics
	LotCount     int64 `json:"lot_count"`
	LotLogCount  int64 `json:"lot_log_count"`
	ConfigCount  int64 `json:"config_count"`
	ProjectCount int64 `json:"project_count"`

	// Pending changes
	PendingChanges []localdb.PendingChange `json:"pending_changes"`

	// Errors
	ErrorCount int         `json:"error_count"`
	Errors     []SyncError `json:"errors,omitempty"`

	// Readiness guard
	Readiness *sync.SyncReadiness `json:"readiness,omitempty"`
}

type SyncUsersStatusResponse struct {
	IsOnline                bool                  `json:"is_online"`
	AutoSyncIntervalSeconds int64                 `json:"auto_sync_interval_seconds"`
	OnlineThresholdSeconds  int64                 `json:"online_threshold_seconds"`
	GeneratedAt             time.Time             `json:"generated_at"`
	Users                   []sync.UserSyncStatus `json:"users"`
}

// SyncError represents a sync error
type SyncError struct {
	Timestamp time.Time `json:"timestamp"`
	Message   string    `json:"message"`
}

// GetInfo returns sync information for modal
// GET /api/sync/info
func (h *SyncHandler) GetInfo(c *gin.Context) {
	connStatus := h.connMgr.GetStatus()
	isOnline := connStatus.IsConnected && strings.TrimSpace(connStatus.LastError) == ""

	// Get DB connection info
	var dbHost, dbUser, dbName string
	if settings, err := h.localDB.GetSettings(); err == nil {
		dbHost = settings.Host + ":" + fmt.Sprintf("%d", settings.Port)
		dbUser = settings.User
		dbName = settings.Database
	}

	// Get sync times
	lastPricelistSync := h.localDB.GetLastSyncTime()
	lastPricelistAttemptAt := h.localDB.GetLastPricelistSyncAttemptAt()
	lastPricelistSyncStatus := h.localDB.GetLastPricelistSyncStatus()
	lastPricelistSyncError := h.localDB.GetLastPricelistSyncError()
	hasFailedSync := strings.EqualFold(lastPricelistSyncStatus, "failed")
	needPricelistSync := lastPricelistSync == nil || hasFailedSync
	hasIncompleteServerSync := hasFailedSync

	// Get local counts
	configCount := h.localDB.CountConfigurations()
	projectCount := h.localDB.CountProjects()
	componentCount := h.localDB.CountLocalComponents()
	pricelistCount := h.localDB.CountLocalPricelists()

	// Get error count (only changes with LastError != "")
	errorCount := int(h.localDB.CountErroredChanges())

	// Get pending changes
	changes, err := h.localDB.GetPendingChanges()
	if err != nil {
		slog.Error("failed to get pending changes for sync info", "error", err)
		changes = []localdb.PendingChange{}
	}

	var syncErrors []SyncError
	for _, change := range changes {
		if change.LastError != "" {
			syncErrors = append(syncErrors, SyncError{
				Timestamp: change.CreatedAt,
				Message:   change.LastError,
			})
		}
	}

	// Limit to last 10 errors
	if len(syncErrors) > 10 {
		syncErrors = syncErrors[:10]
	}

	readiness := h.getReadinessLocal()

	c.JSON(http.StatusOK, SyncInfoResponse{
		DBHost:                  dbHost,
		DBUser:                  dbUser,
		DBName:                  dbName,
		IsOnline:                isOnline,
		LastSyncAt:              lastPricelistSync,
		LastPricelistAttemptAt:  lastPricelistAttemptAt,
		LastPricelistSyncStatus: lastPricelistSyncStatus,
		LastPricelistSyncError:  lastPricelistSyncError,
		NeedPricelistSync:       needPricelistSync,
		HasIncompleteServerSync: hasIncompleteServerSync,
		LotCount:                componentCount,
		LotLogCount:             pricelistCount,
		ConfigCount:             configCount,
		ProjectCount:            projectCount,
		PendingChanges:          changes,
		ErrorCount:              errorCount,
		Errors:                  syncErrors,
		Readiness:               readiness,
	})
}

// GetUsersStatus returns last sync timestamps for users with sync heartbeats.
// GET /api/sync/users-status
func (h *SyncHandler) GetUsersStatus(c *gin.Context) {
	threshold := time.Duration(float64(h.autoSyncInterval) * h.onlineGraceFactor)
	isOnline := h.checkOnline()

	if !isOnline {
		c.JSON(http.StatusOK, SyncUsersStatusResponse{
			IsOnline:                false,
			AutoSyncIntervalSeconds: int64(h.autoSyncInterval.Seconds()),
			OnlineThresholdSeconds:  int64(threshold.Seconds()),
			GeneratedAt:             time.Now().UTC(),
			Users:                   []sync.UserSyncStatus{},
		})
		return
	}

	users, err := h.syncService.ListUserSyncStatuses(threshold)
	if err != nil {
		RespondError(c, http.StatusInternalServerError, "internal server error", err)
		return
	}

	c.JSON(http.StatusOK, SyncUsersStatusResponse{
		IsOnline:                true,
		AutoSyncIntervalSeconds: int64(h.autoSyncInterval.Seconds()),
		OnlineThresholdSeconds:  int64(threshold.Seconds()),
		GeneratedAt:             time.Now().UTC(),
		Users:                   users,
	})
}

// SyncStatusPartial renders the sync status partial for htmx
// GET /partials/sync-status
func (h *SyncHandler) SyncStatusPartial(c *gin.Context) {
	// Check online status from middleware
	isOfflineValue, exists := c.Get("is_offline")
	isOffline := false
	if exists {
		isOffline = isOfflineValue.(bool)
	} else {
		// Fallback: check directly if middleware didn't set it
		isOffline = !h.checkOnline()
		slog.Warn("is_offline not found in context, checking directly")
	}

	// Get pending count
	pendingCount := h.localDB.GetPendingCount()
	readiness := h.getReadinessLocal()
	isBlocked := readiness != nil && readiness.Blocked
	lastPricelistSyncStatus := h.localDB.GetLastPricelistSyncStatus()
	lastPricelistSyncError := h.localDB.GetLastPricelistSyncError()
	hasFailedSync := strings.EqualFold(lastPricelistSyncStatus, "failed")
	hasIncompleteServerSync := hasFailedSync

	slog.Debug("rendering sync status", "is_offline", isOffline, "pending_count", pendingCount, "sync_blocked", isBlocked)

	data := gin.H{
		"IsOffline":               isOffline,
		"PendingCount":            pendingCount,
		"IsBlocked":               isBlocked,
		"HasFailedSync":           hasFailedSync,
		"HasIncompleteServerSync": hasIncompleteServerSync,
		"SyncIssueTitle": func() string {
			if hasIncompleteServerSync {
				return "Последняя синхронизация прайслистов прервалась. На сервере есть изменения, которые не загружены локально."
			}
			if hasFailedSync {
				if lastPricelistSyncError != "" {
					return lastPricelistSyncError
				}
				return "Последняя синхронизация прайслистов завершилась ошибкой."
			}
			return ""
		}(),
		"BlockedReason": func() string {
			if readiness == nil {
				return ""
			}
			return readiness.ReasonText
		}(),
	}

	c.Header("Content-Type", "text/html; charset=utf-8")
	if err := h.tmpl.ExecuteTemplate(c.Writer, "sync_status", data); err != nil {
		slog.Error("failed to render sync_status template", "error", err)
		_ = c.Error(err)
		c.String(http.StatusInternalServerError, "Template error")
	}
}

func (h *SyncHandler) getReadinessLocal() *sync.SyncReadiness {
	h.readinessMu.Lock()
	if h.readinessCached != nil && time.Since(h.readinessCachedAt) < 10*time.Second {
		cached := *h.readinessCached
		h.readinessMu.Unlock()
		return &cached
	}
	h.readinessMu.Unlock()

	state, err := h.localDB.GetSyncGuardState()
	if err != nil || state == nil {
		return nil
	}

	// OFFLINE_UNVERIFIED_SCHEMA is only valid while actually offline.
	// Suppress it when the connection manager reports online so the stale
	// blocked state from a previous disconnection doesn't linger in the UI.
	if state.ReasonCode == "OFFLINE_UNVERIFIED_SCHEMA" && h.checkOnline() {
		return nil
	}

	readiness := &sync.SyncReadiness{
		Status:                state.Status,
		Blocked:               state.Status == sync.ReadinessBlocked,
		ReasonCode:            state.ReasonCode,
		ReasonText:            state.ReasonText,
		RequiredMinAppVersion: state.RequiredMinAppVersion,
		LastCheckedAt:         state.LastCheckedAt,
	}

	h.readinessMu.Lock()
	h.readinessCached = readiness
	h.readinessCachedAt = time.Now()
	h.readinessMu.Unlock()
	return readiness
}

// ReportPartnumberSeen pushes unresolved vendor partnumbers to qt_vendor_partnumber_seen on MariaDB.
// POST /api/sync/partnumber-seen
func (h *SyncHandler) ReportPartnumberSeen(c *gin.Context) {
	var body struct {
		Items []struct {
			Partnumber  string `json:"partnumber"`
			Description string `json:"description"`
			Ignored     bool   `json:"ignored"`
		} `json:"items"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		RespondError(c, http.StatusUnprocessableEntity, "invalid request", err)
		return
	}

	items := make([]sync.SeenPartnumber, 0, len(body.Items))
	for _, it := range body.Items {
		if it.Partnumber != "" {
			items = append(items, sync.SeenPartnumber{
				Partnumber:  it.Partnumber,
				Description: it.Description,
				Ignored:     it.Ignored,
			})
		}
	}

	if err := h.syncService.PushPartnumberSeen(items); err != nil {
		RespondError(c, http.StatusServiceUnavailable, "service unavailable", err)
		return
	}

	c.JSON(http.StatusOK, gin.H{"reported": len(items)})
}
