package api

import (
	"encoding/json"
	"errors"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"

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

// The settings history: every revision of one person's synced settings, what each one
// changed, which televisions have taken it, and how to put an old one back.
//
// It exists because the revision is a delivery mechanism with no receipt. /v1/status hands
// every open television a number and the set fetches the document when it differs — which
// works, and tells the operator nothing. "I changed their card size an hour ago and the
// bedroom TV still looks wrong" has three possible answers (it never fetched, it fetched
// and something else has since overwritten it, or it is fine and the complaint is about
// something else) and no way to tell them apart. Acks are what separate them.
//
// Nothing here rewinds a revision. A restore is a *new* revision carrying an old document,
// for the same reason the counter only ever goes up: a television compares numbers, so one
// that went backwards would leave every set in the house believing it was already up to
// date and holding settings the operator had just replaced.

// adminHistoryLimit is how many revisions the page asks for. The store caps it too; this
// is the page saying what it can usefully draw.
const adminHistoryLimit = 60

type adminPreferenceRevision struct {
	Revision int64  `json:"revision"`
	Source   string `json:"source"`
	// Author is who made the change, in the console's words: a television's name, or the
	// admin console itself. Recorded at write time, so a set signed out since still has
	// a name here.
	Author        string             `json:"author"`
	DeviceID      string             `json:"deviceId,omitempty"`
	ClientVersion string             `json:"clientVersion,omitempty"`
	RestoredFrom  int64              `json:"restoredFrom,omitempty"`
	CreatedAt     time.Time          `json:"createdAt"`
	Current       bool               `json:"current"`
	Changes       []preferenceChange `json:"changes"`
	// Initial marks the first revision this history holds. Its "changes" are measured
	// against the defaults, and calling that a change would be a claim about a decision
	// nobody made — so the page labels it rather than listing it.
	Initial     bool                  `json:"initial"`
	Preferences map[string]any        `json:"preferences"`
	Acks        []store.PreferenceAck `json:"acks"`
}

// adminPreferenceDevice is one television's position: which revision it holds, and how far
// that is behind the current one.
type adminPreferenceDevice struct {
	DeviceID      string    `json:"deviceId"`
	Name          string    `json:"name"`
	ClientVersion string    `json:"clientVersion,omitempty"`
	LastSeen      time.Time `json:"lastSeen"`
	SignedOut     bool      `json:"signedOut"`
	Revision      int64     `json:"revision"`
	AckedAt       any       `json:"ackedAt,omitempty"`
	// Behind is how many revisions this set has yet to take. Zero is up to date; the
	// field is what the page sorts and colours by.
	Behind int64 `json:"behind"`
	// Never is a set that has never fetched the document at all — a fresh sign-in, or a
	// build that predates synced settings. Distinct from being behind, because there is
	// nothing to roll back to for it.
	Never bool `json:"never"`
}

// handleAdminPreferenceHistory answers the whole page in one request: the revisions, what
// each changed, and where every television has got to.
func (s *Server) handleAdminPreferenceHistory(w http.ResponseWriter, r *http.Request) {
	userID := strings.TrimSpace(r.PathValue("userID"))
	if userID == "" {
		writeError(w, http.StatusBadRequest, "user is required")
		return
	}

	history, err := s.store.UserPreferenceHistory(r.Context(), userID, adminHistoryLimit)
	if err != nil {
		s.loggerFor(r.Context()).Error("preference history read failed", "user", userID, "error", err)
		writeError(w, http.StatusInternalServerError, "could not read the settings history")
		return
	}
	current, err := s.store.UserPreferences(r.Context(), userID)
	if err != nil {
		s.loggerFor(r.Context()).Error("preference read failed", "user", userID, "error", err)
		writeError(w, http.StatusInternalServerError, "could not read these settings")
		return
	}

	// One account read for both the person's name and their televisions. A failure costs
	// the page its device names, never the history itself — which is the half that cannot
	// be reconstructed from anywhere else.
	account, err := s.membyAccount(r, userID)
	if err != nil {
		s.loggerFor(r.Context()).Warn("account unavailable for settings history", "error", err)
	}

	writeJSON(w, http.StatusOK, map[string]any{
		"userId":          userID,
		"username":        account.Username,
		"currentRevision": current.Revision,
		"currentSource":   current.Source,
		"updatedAt":       optionalTime(current.UpdatedAt),
		"saved":           current.Revision > 0,
		"revisions":       adminRevisions(history, current.Revision),
		"devices":         s.adminPreferenceDevices(r, userID, account.Devices, current.Revision),
		"catalogue":       preferenceCatalogue,
		"schemaVersion":   preferenceSchemaVersion,
	})
}

// adminRevisions turns stored revisions into what the table draws. It is a pure function
// of the history so it can be tested without a database, which is the only way to pin the
// two rules that are easy to get wrong: the diff is against the revision *below* this one,
// and the oldest entry held has nothing below it to be a diff of.
func adminRevisions(history []store.PreferenceRevision, currentRevision int64) []adminPreferenceRevision {
	result := make([]adminPreferenceRevision, 0, len(history))
	for index, entry := range history {
		document := decodePreferences(entry.Preferences)
		// history is newest first, so the predecessor is the next element along.
		initial := index == len(history)-1
		changes := []preferenceChange{}
		if !initial {
			changes = preferenceChanges(decodePreferences(history[index+1].Preferences), document)
		}
		acks := entry.Acks
		if acks == nil {
			acks = []store.PreferenceAck{}
		}
		result = append(result, adminPreferenceRevision{
			Revision: entry.Revision, Source: entry.Source,
			Author: revisionAuthor(entry), DeviceID: entry.DeviceID,
			ClientVersion: entry.ClientVersion, RestoredFrom: entry.RestoredFrom,
			CreatedAt: entry.CreatedAt, Current: entry.Revision == currentRevision,
			Changes: changes, Initial: initial, Preferences: document, Acks: acks,
		})
	}
	return result
}

// revisionAuthor names who made a change in the console's own vocabulary. A device row
// with no name is an older write, from before the history recorded one — "a television" is
// honest where inventing a name would not be.
func revisionAuthor(entry store.PreferenceRevision) string {
	switch {
	case entry.Source == "admin":
		return "Admin console"
	case entry.DeviceName != "":
		return entry.DeviceName
	case entry.Source == "device":
		return "A television"
	default:
		return entry.Source
	}
}

// adminPreferenceDevices lists every television signed in to this account beside the
// revision it holds. Signed-in sets come first because they are the ones an operator can
// still expect to catch up; a set that has acked but is no longer signed in is kept so its
// last known position is not silently dropped.
func (s *Server) adminPreferenceDevices(
	r *http.Request, userID string, known []store.MembyDevice, currentRevision int64,
) []adminPreferenceDevice {
	states, err := s.store.PreferenceDeviceStates(r.Context(), userID)
	if err != nil {
		s.loggerFor(r.Context()).Warn("preference device states unavailable", "error", err)
		states = map[string]store.PreferenceDeviceState{}
	}

	devices := []adminPreferenceDevice{}
	seen := map[string]bool{}
	for _, device := range known {
		state, acked := states[device.ID]
		seen[device.ID] = true
		devices = append(devices, adminPreferenceDevice{
			DeviceID: device.ID, Name: device.Name, ClientVersion: device.Version,
			LastSeen: device.LastSeen, Revision: state.Revision,
			AckedAt: optionalTime(state.AckedAt), Never: !acked,
			Behind: behindBy(currentRevision, state.Revision),
		})
	}
	for deviceID, state := range states {
		if seen[deviceID] {
			continue
		}
		devices = append(devices, adminPreferenceDevice{
			DeviceID: deviceID, Name: "Signed-out television", SignedOut: true,
			Revision: state.Revision, AckedAt: optionalTime(state.AckedAt),
			Behind: behindBy(currentRevision, state.Revision),
		})
	}
	sort.SliceStable(devices, func(i, j int) bool {
		if devices[i].SignedOut != devices[j].SignedOut {
			return devices[j].SignedOut
		}
		if devices[i].Behind != devices[j].Behind {
			return devices[i].Behind > devices[j].Behind
		}
		return strings.ToLower(devices[i].Name) < strings.ToLower(devices[j].Name)
	})
	return devices
}

// behindBy never goes negative. A television holding a revision above the current one is
// impossible in ordinary operation, but the counter is the whole delivery mechanism and a
// negative "behind" on the page would read as nonsense rather than as the anomaly it is.
func behindBy(current, held int64) int64 {
	if held >= current {
		return 0
	}
	return current - held
}

func optionalTime(value time.Time) any {
	if value.IsZero() {
		return nil
	}
	return value
}

// membyAccount is one person out of the same list the console's other pages read. Somebody
// with no signed-in television is not an error: their settings history outlives their
// devices, and is one of the more useful times to be able to read it.
func (s *Server) membyAccount(r *http.Request, userID string) (store.MembyAccount, error) {
	accounts, err := s.store.MembyAccounts(r.Context())
	if err != nil {
		return store.MembyAccount{}, err
	}
	for _, account := range accounts {
		if account.ID == userID {
			return account, nil
		}
	}
	return store.MembyAccount{}, nil
}

// handleAdminRestorePreferences puts an earlier document back.
//
// It is a forward write, never a rewind: the restored document goes out as the next
// revision, attributed to the console and recording what it was taken from. That is what
// makes it visible to televisions at all — they compare revision numbers — and it is also
// what makes a restore itself undoable, since the revision it replaced is still sitting in
// the history one row down.
//
// The document is re-normalised on the way out. A revision written before a setting
// existed has nothing to say about it, and the catalogue's default is the right answer
// there; a revision written before a setting's options changed may hold one that is no
// longer legal, and restoring it verbatim would put a value on a television that the
// server itself would reject.
func (s *Server) handleAdminRestorePreferences(w http.ResponseWriter, r *http.Request) {
	userID := strings.TrimSpace(r.PathValue("userID"))
	revision, err := strconv.ParseInt(strings.TrimSpace(r.PathValue("revision")), 10, 64)
	if userID == "" || err != nil || revision <= 0 {
		writeError(w, http.StatusBadRequest, "user and revision are required")
		return
	}

	document, err := s.store.PreferenceRevisionDocument(r.Context(), userID, revision)
	if errors.Is(err, store.ErrNotFound) {
		writeError(w, http.StatusNotFound, "that revision is no longer held")
		return
	}
	if err != nil {
		s.loggerFor(r.Context()).Error("preference revision read failed",
			"user", userID, "revision", revision, "error", err)
		writeError(w, http.StatusInternalServerError, "could not read that revision")
		return
	}

	values := map[string]any{}
	_ = json.Unmarshal(document, &values)
	raw, err := json.Marshal(normalizePreferences(values))
	if err != nil {
		writeError(w, http.StatusInternalServerError, "could not rebuild that revision")
		return
	}

	// ForceRevision for the same reason an ordinary push takes it: every one of this
	// person's televisions is also a writer of this row, and an operator deliberately
	// restoring a version must not lose a race to whichever set syncs next.
	stored, err := s.store.SetUserPreferences(r.Context(), userID, store.PreferenceWrite{
		Preferences: raw, ExpectedRevision: store.ForceRevision,
		Source: "admin", RestoredFrom: revision,
	})
	if err != nil {
		s.loggerFor(r.Context()).Error("preference restore failed",
			"user", userID, "revision", revision, "error", err)
		writeError(w, http.StatusInternalServerError, "could not restore that revision")
		return
	}
	s.loggerFor(r.Context()).Info("settings restored",
		"user", userID, "revision", stored.Revision, "restoredFrom", revision, "source", "admin")
	writeJSON(w, http.StatusOK, preferencesPayload(stored))
}
