package sync

import (
	"context"
	"log"
	"time"

	"tps.local/labbridge-agent/internal/queue"
)

// Worker owns the periodic sync loop:
//
//  1. drain the result queue (push each pending message; idempotent on UUID)
//  2. drain the event queue (one batch POST)
//  3. pull the worklist cache
//  4. send a manifest reconcile (cloud tells us anything it's missing)
//
// All four steps are best-effort: a single transient cloud failure pauses
// the cycle, queue items stay 'pending', and the next tick retries.
type Worker struct {
	store    *queue.Store
	client   *Client
	interval time.Duration
}

func NewWorker(store *queue.Store, client *Client, interval time.Duration) *Worker {
	return &Worker{store: store, client: client, interval: interval}
}

// Run blocks until ctx is cancelled, running one sweep immediately then on
// every tick.
func (w *Worker) Run(ctx context.Context) {
	w.sweep()
	t := time.NewTicker(w.interval)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-t.C:
			w.sweep()
		}
	}
}

// SweepNow runs one sweep on demand — call right after a new local message is
// queued so the cloud sees it without waiting for the next tick.
func (w *Worker) SweepNow() { go w.sweep() }

func (w *Worker) sweep() {
	if !w.pushResults() {
		return // cloud is unreachable; skip the rest of the cycle
	}
	w.pushEvents()
	w.pullWorklist()
	w.reconcileManifest()
}

// pushResults drains pending queue items in order. Returns false on the first
// non-validation error (network failure) so the caller can abort the cycle.
func (w *Worker) pushResults() bool {
	items, err := w.store.PendingResults(100)
	if err != nil {
		log.Printf("sync: list pending results: %v", err)
		return false
	}
	for _, it := range items {
		resp, err := w.client.PushResult(it.UUID, it.Vendor, it.RawMessage, it.DeviceID, it.ReceivedAt)
		if err != nil {
			_ = w.store.MarkFailed(it.ID, err.Error())
			log.Printf("sync: push result %s: %v", it.UUID, err)
			return false
		}
		// "stored", "duplicate" and even "not_a_result" all mean the cloud
		// owns the message now — we can mark it synced and move on.
		_ = w.store.MarkSynced(it.ID)
		log.Printf("sync: pushed %s -> %s (session %d, %d results)",
			it.UUID, resp.Status, resp.SessionID, resp.Results)
	}
	return true
}

func (w *Worker) pushEvents() {
	events, err := w.store.PendingEvents(200)
	if err != nil {
		log.Printf("sync: list pending events: %v", err)
		return
	}
	if len(events) == 0 {
		return
	}
	payload := make([]map[string]interface{}, 0, len(events))
	ids := make([]int64, 0, len(events))
	for _, e := range events {
		row := map[string]interface{}{
			"type":      e.Type,
			"level":     e.Level,
			"category":  e.Category,
			"vendor":    e.Vendor,
			"device_ip": e.DeviceIP,
			"sample_no": e.SampleNo,
			"message":   e.Message,
		}
		if e.DeviceID != nil {
			row["device_id"] = *e.DeviceID
		}
		if e.Context != nil {
			row["context"] = e.Context
		}
		payload = append(payload, row)
		ids = append(ids, e.ID)
	}
	if err := w.client.PushEvents(payload); err != nil {
		log.Printf("sync: push events: %v", err)
		return
	}
	if err := w.store.MarkEventsSynced(ids); err != nil {
		log.Printf("sync: mark events synced: %v", err)
	}
}

func (w *Worker) pullWorklist() {
	wl, err := w.client.PullWorklist()
	if err != nil {
		log.Printf("sync: pull worklist: %v", err)
		return
	}
	if err := w.store.ReplaceWorklist(wl); err != nil {
		log.Printf("sync: cache worklist: %v", err)
	}
}

// reconcileManifest belts-and-suspenders the idempotent push: if the cloud
// reports it is missing some UUIDs we think we synced (e.g. a write was
// rolled back), flip them back to pending so the next sweep re-pushes them.
func (w *Worker) reconcileManifest() {
	uuids, err := w.store.RecentSyncedUUIDs(200)
	if err != nil || len(uuids) == 0 {
		return
	}
	missing, err := w.client.SyncManifest(uuids)
	if err != nil {
		log.Printf("sync: manifest reconcile: %v", err)
		return
	}
	if len(missing) > 0 {
		log.Printf("sync: cloud missing %d uuids, re-queuing", len(missing))
		_ = w.store.ReQueueByUUIDs(missing)
	}
}
