package store

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/jackc/pgx/v5"
)

// UserPreferences is one viewer's TV settings, held server-side so they follow the person
// rather than the television.
//
// The document is stored opaquely on purpose. The vocabulary — which keys exist, what
// values are legal — lives in internal/api, next to the client contract it belongs to, so
// adding a setting is one file and never a database migration. What the store owns is the
// part the API cannot: the revision, and the guarantee that two writers cannot interleave.
type UserPreferences struct {
	Preferences json.RawMessage `json:"preferences"`
	// Revision increases by one on every accepted write. It is what lets a television
	// notice, from the status poll alone, that something changed it elsewhere.
	Revision  int64     `json:"revision"`
	UpdatedAt time.Time `json:"updatedAt"`
	// Source records who wrote it last: "device" or "admin". The TV shows nothing with
	// this, but an operator asking "why did their layout change" needs the answer.
	Source string `json:"source"`
}

// ErrPreferencesConflict means the caller's revision was not the current one. The client
// resolves it by reading the server's copy — never by retrying its own write, which is
// how an admin push would get silently undone by whichever TV was slowest to notice.
var ErrPreferencesConflict = errors.New("store: user preferences revision conflict")

// ForceRevision skips the revision check. Only the admin console passes it: an operator
// pushing settings is deliberately overriding whatever the devices last agreed on.
const ForceRevision int64 = -1

func emptyPreferences() UserPreferences {
	return UserPreferences{Preferences: json.RawMessage(`{}`), Source: "default"}
}

func (s *Store) UserPreferences(ctx context.Context, userID string) (UserPreferences, error) {
	var result UserPreferences
	var raw []byte
	err := s.pool.QueryRow(ctx, `
		SELECT preferences, revision, updated_at, source
		FROM user_preferences WHERE emby_user_id = $1`, userID).
		Scan(&raw, &result.Revision, &result.UpdatedAt, &result.Source)
	if errors.Is(err, pgx.ErrNoRows) {
		return emptyPreferences(), nil
	}
	if err != nil {
		return emptyPreferences(), fmt.Errorf("store: read user preferences: %w", err)
	}
	result.Preferences = json.RawMessage(raw)
	return result, nil
}

// AllUserPreferences is the admin console's read. Absent users are absent rather than
// defaulted, so the page can distinguish "never saved anything" from "saved the defaults".
func (s *Store) AllUserPreferences(ctx context.Context) (map[string]UserPreferences, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT emby_user_id, preferences, revision, updated_at, source FROM user_preferences`)
	if err != nil {
		return nil, fmt.Errorf("store: list user preferences: %w", err)
	}
	defer rows.Close()
	all := map[string]UserPreferences{}
	for rows.Next() {
		var userID string
		var value UserPreferences
		var raw []byte
		if err := rows.Scan(&userID, &raw, &value.Revision, &value.UpdatedAt, &value.Source); err != nil {
			return nil, fmt.Errorf("store: scan user preferences: %w", err)
		}
		value.Preferences = json.RawMessage(raw)
		all[userID] = value
	}
	return all, rows.Err()
}

// PreferenceWrite is one attempt to change somebody's settings, and everything the
// history needs to describe it afterwards.
//
// The attribution fields are on the write rather than looked up later because the answer
// changes: a television is renamed, signed out, replaced. What the history has to be able
// to say is what was true when the change was made.
type PreferenceWrite struct {
	Preferences json.RawMessage
	// ExpectedRevision is the revision the writer believes it is editing, or
	// ForceRevision for a deliberate override.
	ExpectedRevision int64
	// Source is "device" or "admin".
	Source                              string
	DeviceID, DeviceName, ClientVersion string
	// RestoredFrom is the revision this document was taken from, when the write is a
	// restore. Zero otherwise.
	RestoredFrom int64
}

// preferenceHistoryLimit is how many revisions are kept per person. This is a household:
// a hundred changes is years of them, and the operator question history answers — "what
// did I just do to their launcher, and can I undo it" — is asked about the recent end.
const preferenceHistoryLimit = 100

// SetUserPreferences writes the document if write.ExpectedRevision still matches, and
// returns what is now stored.
//
// The advisory lock and the re-read inside the transaction are the point: every
// television in the house writes this row, and an admin push lands in the middle of them.
// Without the lock, two TVs that both read revision 4 would both write revision 5 and one
// person's choice would vanish with no error anywhere.
//
// The history row is written in the same transaction as the document, so there is no
// state in which a revision exists and nothing records where it came from.
func (s *Store) SetUserPreferences(
	ctx context.Context, userID string, write PreferenceWrite,
) (UserPreferences, error) {
	preferences, expectedRevision, source := write.Preferences, write.ExpectedRevision, write.Source
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return UserPreferences{}, fmt.Errorf("store: begin user preferences write: %w", err)
	}
	defer func() { _ = tx.Rollback(ctx) }()
	if _, err := tx.Exec(ctx,
		`SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, "prefs:"+userID); err != nil {
		return UserPreferences{}, fmt.Errorf("store: lock user preferences: %w", err)
	}

	var current int64
	err = tx.QueryRow(ctx,
		`SELECT revision FROM user_preferences WHERE emby_user_id = $1`, userID).Scan(&current)
	if err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return UserPreferences{}, fmt.Errorf("store: read current user preferences: %w", err)
	}
	if expectedRevision != ForceRevision && current != expectedRevision {
		return UserPreferences{}, ErrPreferencesConflict
	}

	next := UserPreferences{
		Preferences: preferences, Revision: current + 1,
		UpdatedAt: time.Now().UTC(), Source: source,
	}
	if _, err := tx.Exec(ctx, `
		INSERT INTO user_preferences (emby_user_id, preferences, revision, updated_at, source)
		VALUES ($1, $2::jsonb, $3, $4, $5)
		ON CONFLICT (emby_user_id) DO UPDATE SET
			preferences = EXCLUDED.preferences, revision = EXCLUDED.revision,
			updated_at = EXCLUDED.updated_at, source = EXCLUDED.source`,
		userID, string(preferences), next.Revision, next.UpdatedAt, source); err != nil {
		return UserPreferences{}, fmt.Errorf("store: write user preferences: %w", err)
	}

	var restoredFrom any
	if write.RestoredFrom > 0 {
		restoredFrom = write.RestoredFrom
	}
	if _, err := tx.Exec(ctx, `
		INSERT INTO user_preference_revisions (
			emby_user_id, revision, preferences, source,
			device_id, device_name, client_version, restored_from, created_at)
		VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7, $8, $9)
		ON CONFLICT (emby_user_id, revision) DO NOTHING`,
		userID, next.Revision, string(preferences), source,
		write.DeviceID, write.DeviceName, write.ClientVersion, restoredFrom, next.UpdatedAt,
	); err != nil {
		return UserPreferences{}, fmt.Errorf("store: write preference history: %w", err)
	}
	// Pruned here rather than on a schedule: this is the only writer, so it is the only
	// place the table can grow, and the acks of a revision nobody can see any more are
	// dead weight with it.
	if _, err := tx.Exec(ctx, `
		DELETE FROM user_preference_revisions
		WHERE emby_user_id = $1 AND revision <= $2 - $3`,
		userID, next.Revision, preferenceHistoryLimit,
	); err != nil {
		return UserPreferences{}, fmt.Errorf("store: prune preference history: %w", err)
	}
	// A device's most recent ack is never pruned, however far behind it has fallen. A
	// television switched off for a year is exactly the one an operator wants to see
	// described as "on revision 12" rather than as one that has never checked in.
	if _, err := tx.Exec(ctx, `
		DELETE FROM user_preference_acks stale
		WHERE stale.emby_user_id = $1 AND stale.revision <= $2 - $3
		  AND EXISTS (
			SELECT 1 FROM user_preference_acks newer
			WHERE newer.emby_user_id = stale.emby_user_id
			  AND newer.device_id = stale.device_id
			  AND newer.revision > stale.revision)`,
		userID, next.Revision, preferenceHistoryLimit,
	); err != nil {
		return UserPreferences{}, fmt.Errorf("store: prune preference acks: %w", err)
	}

	if err := tx.Commit(ctx); err != nil {
		return UserPreferences{}, fmt.Errorf("store: commit user preferences: %w", err)
	}
	return next, nil
}

// PreferenceRevision is one entry in the history: what was stored, who stored it, and
// which televisions have since taken it.
type PreferenceRevision struct {
	Revision      int64           `json:"revision"`
	Preferences   json.RawMessage `json:"-"`
	Source        string          `json:"source"`
	DeviceID      string          `json:"deviceId,omitempty"`
	DeviceName    string          `json:"deviceName,omitempty"`
	ClientVersion string          `json:"clientVersion,omitempty"`
	RestoredFrom  int64           `json:"restoredFrom,omitempty"`
	CreatedAt     time.Time       `json:"createdAt"`
	Acks          []PreferenceAck `json:"acks"`
}

// PreferenceAck is one television taking one revision.
type PreferenceAck struct {
	DeviceID      string    `json:"deviceId"`
	DeviceName    string    `json:"deviceName"`
	ClientVersion string    `json:"clientVersion,omitempty"`
	Revision      int64     `json:"revision"`
	AckedAt       time.Time `json:"ackedAt"`
}

// UserPreferenceHistory returns the newest revisions first, each carrying the televisions
// that fetched it.
//
// Two queries rather than a join: the history is small and bounded, and a join would
// return the document — the largest column here — once per acknowledging device.
func (s *Store) UserPreferenceHistory(
	ctx context.Context, userID string, limit int,
) ([]PreferenceRevision, error) {
	if limit <= 0 || limit > preferenceHistoryLimit {
		limit = preferenceHistoryLimit
	}
	rows, err := s.pool.Query(ctx, `
		SELECT revision, preferences, source, device_id, device_name,
			client_version, COALESCE(restored_from, 0), created_at
		FROM user_preference_revisions
		WHERE emby_user_id = $1
		ORDER BY revision DESC
		LIMIT $2`, userID, limit)
	if err != nil {
		return nil, fmt.Errorf("store: read preference history: %w", err)
	}
	defer rows.Close()

	history := []PreferenceRevision{}
	byRevision := map[int64]int{}
	for rows.Next() {
		var entry PreferenceRevision
		var raw []byte
		if err := rows.Scan(&entry.Revision, &raw, &entry.Source, &entry.DeviceID,
			&entry.DeviceName, &entry.ClientVersion, &entry.RestoredFrom, &entry.CreatedAt,
		); err != nil {
			return nil, fmt.Errorf("store: scan preference history: %w", err)
		}
		entry.Preferences = json.RawMessage(raw)
		entry.Acks = []PreferenceAck{}
		byRevision[entry.Revision] = len(history)
		history = append(history, entry)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("store: read preference history: %w", err)
	}
	if len(history) == 0 {
		return history, nil
	}

	oldest := history[len(history)-1].Revision
	ackRows, err := s.pool.Query(ctx, `
		SELECT device_id, device_name, client_version, revision, acked_at
		FROM user_preference_acks
		WHERE emby_user_id = $1 AND revision >= $2
		ORDER BY acked_at`, userID, oldest)
	if err != nil {
		return nil, fmt.Errorf("store: read preference acks: %w", err)
	}
	defer ackRows.Close()
	for ackRows.Next() {
		var ack PreferenceAck
		if err := ackRows.Scan(&ack.DeviceID, &ack.DeviceName,
			&ack.ClientVersion, &ack.Revision, &ack.AckedAt); err != nil {
			return nil, fmt.Errorf("store: scan preference ack: %w", err)
		}
		if index, ok := byRevision[ack.Revision]; ok {
			history[index].Acks = append(history[index].Acks, ack)
		}
	}
	return history, ackRows.Err()
}

// PreferenceRevisionDocument is the stored document for one revision, which is what a
// restore reads. Absent means pruned or never written, and the caller must say so rather
// than silently restoring something else.
func (s *Store) PreferenceRevisionDocument(
	ctx context.Context, userID string, revision int64,
) (json.RawMessage, error) {
	var raw []byte
	err := s.pool.QueryRow(ctx, `
		SELECT preferences FROM user_preference_revisions
		WHERE emby_user_id = $1 AND revision = $2`, userID, revision).Scan(&raw)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, ErrNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("store: read preference revision: %w", err)
	}
	return json.RawMessage(raw), nil
}

// PreferenceDeviceState is where one television has got to: the newest revision it has
// fetched. A set that has never fetched one is absent, which the caller reports as such —
// "has not taken anything yet" and "is on revision 3" are different situations.
type PreferenceDeviceState struct {
	DeviceID string    `json:"deviceId"`
	Revision int64     `json:"revision"`
	AckedAt  time.Time `json:"ackedAt"`
}

func (s *Store) PreferenceDeviceStates(
	ctx context.Context, userID string,
) (map[string]PreferenceDeviceState, error) {
	rows, err := s.pool.Query(ctx, `
		SELECT DISTINCT ON (device_id) device_id, revision, acked_at
		FROM user_preference_acks
		WHERE emby_user_id = $1
		ORDER BY device_id, revision DESC`, userID)
	if err != nil {
		return nil, fmt.Errorf("store: read preference device states: %w", err)
	}
	defer rows.Close()
	states := map[string]PreferenceDeviceState{}
	for rows.Next() {
		var state PreferenceDeviceState
		if err := rows.Scan(&state.DeviceID, &state.Revision, &state.AckedAt); err != nil {
			return nil, fmt.Errorf("store: scan preference device state: %w", err)
		}
		states[state.DeviceID] = state
	}
	return states, rows.Err()
}

// RecordPreferenceAck notes that one television now holds one revision.
//
// It is written when a set *fetches* the document, which is the only proof there is that
// it took it: the status poll tells every open TV the revision, but being told is not the
// same as having adopted, and a set that is switched off or cannot reach the gateway is
// exactly the one an operator is asking about.
//
// A device with no id is not recorded rather than recorded as "": the empty id would
// collect every legacy client into one imaginary television.
func (s *Store) RecordPreferenceAck(
	ctx context.Context, userID, deviceID, deviceName, clientVersion string, revision int64,
) error {
	if userID == "" || deviceID == "" || revision <= 0 {
		return nil
	}
	_, err := s.pool.Exec(ctx, `
		INSERT INTO user_preference_acks (
			emby_user_id, device_id, revision, device_name, client_version, acked_at)
		VALUES ($1, $2, $3, $4, $5, now())
		ON CONFLICT (emby_user_id, device_id, revision) DO UPDATE SET
			device_name = EXCLUDED.device_name,
			client_version = EXCLUDED.client_version`,
		userID, deviceID, revision, deviceName, clientVersion)
	if err != nil {
		return fmt.Errorf("store: record preference ack: %w", err)
	}
	return nil
}

// UserPreferenceRevision is what the status poll consults: the revision alone, never the
// document. /v1/status runs every ten seconds for every open television, so this has to
// stay a primary-key lookup returning one number — the TV only needs to know whether to
// go and fetch the rest.
func (s *Store) UserPreferenceRevision(ctx context.Context, userID string) (int64, error) {
	var revision int64
	err := s.pool.QueryRow(ctx,
		`SELECT revision FROM user_preferences WHERE emby_user_id = $1`, userID).Scan(&revision)
	if errors.Is(err, pgx.ErrNoRows) {
		return 0, nil
	}
	if err != nil {
		return 0, fmt.Errorf("store: read preference revision: %w", err)
	}
	return revision, nil
}
