package api

import (
	"context"
	"encoding/json"
	"net/http"
	"time"

	"github.com/ponzischeme89/memby/server/internal/store"
)

// maxAnalyticsBatch caps one upload. The TV batches events and flushes periodically, so
// a larger payload than this means something has gone wrong client-side.
const maxAnalyticsBatch = 200

// maxDwellMs discards implausible dwell times — a TV left on a row overnight says
// nothing about what anyone was looking at.
const maxDwellMs = 30 * 60 * 1000

type rowEventPayload struct {
	RowID      string `json:"rowId"`
	RowKind    string `json:"rowKind"`
	Event      string `json:"event"`
	ItemID     string `json:"itemId"`
	DwellMs    int    `json:"dwellMs"`
	OccurredAt string `json:"occurredAt"`
}

type analyticsRequest struct {
	Events []rowEventPayload `json:"events"`
}

// handleRowAnalytics accepts a batch of row engagement events from a TV.
//
// Fire-and-forget by design: the client does not retry, and a rejected event is never
// worth surfacing on screen. Bad events are dropped individually rather than failing the
// batch.
func (s *Server) handleRowAnalytics(w http.ResponseWriter, r *http.Request, sess store.Session) {
	var req analyticsRequest
	if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "malformed request body")
		return
	}
	if len(req.Events) > maxAnalyticsBatch {
		req.Events = req.Events[:maxAnalyticsBatch]
	}

	now := time.Now().UTC()
	events := make([]store.RowEvent, 0, len(req.Events))
	for _, payload := range req.Events {
		event, ok := toRowEvent(payload, sess.EmbyUserID, now)
		if !ok {
			continue
		}
		events = append(events, event)
	}

	if err := s.store.InsertRowEvents(r.Context(), events); err != nil {
		s.loggerFor(r.Context()).Warn("row analytics write failed", "error", err)
		// Still a 204: telemetry must never make the TV think something is broken.
	}
	for _, event := range events {
		if event.Event == store.RowEventSelect {
			_ = s.cache.InvalidateUser(r.Context(), sess.EmbyUserID)
			if s.forYou != nil {
				s.forYou.MarkDirty(context.WithoutCancel(r.Context()), sess)
			}
			break
		}
	}
	w.WriteHeader(http.StatusNoContent)
}

func toRowEvent(payload rowEventPayload, userID string, now time.Time) (store.RowEvent, bool) {
	if payload.RowID == "" {
		return store.RowEvent{}, false
	}
	switch payload.Event {
	case store.RowEventImpression, store.RowEventFocus, store.RowEventSelect:
	default:
		return store.RowEvent{}, false
	}

	occurredAt := now
	if payload.OccurredAt != "" {
		if parsed, err := time.Parse(time.RFC3339, payload.OccurredAt); err == nil {
			// Trust the device's clock only within a sane window; TVs are notorious for
			// waking up in 1970.
			if parsed.After(now.Add(-24*time.Hour)) && parsed.Before(now.Add(time.Hour)) {
				occurredAt = parsed.UTC()
			}
		}
	}

	dwell := payload.DwellMs
	if dwell < 0 {
		dwell = 0
	}
	if dwell > maxDwellMs {
		dwell = maxDwellMs
	}

	return store.RowEvent{
		OccurredAt: occurredAt,
		UserID:     userID,
		RowID:      payload.RowID,
		RowKind:    payload.RowKind,
		Event:      payload.Event,
		ItemID:     payload.ItemID,
		DwellMs:    dwell,
	}, true
}
