// Package store contains all database repositories.
package store

import (
	"context"
	"errors"
	"fmt"
	"time"

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

// ErrReorderMismatch wird von Reorder zurückgegeben, wenn die übergebene
// ID-Liste nicht mit den tatsächlichen Items der Playlist übereinstimmt.
var ErrReorderMismatch = errors.New("reorder: item list does not match playlist")

// ------------------------------------------------------------------
// Domain types
// ------------------------------------------------------------------

type Tenant struct {
	ID        string    `json:"id"`
	Slug      string    `json:"slug"`
	Name      string    `json:"name"`
	CreatedAt time.Time `json:"created_at"`
}

type Screen struct {
	ID          string    `json:"id"`
	TenantID    string    `json:"tenant_id"`
	Slug        string    `json:"slug"`
	Name        string    `json:"name"`
	Orientation string    `json:"orientation"`
	CreatedAt   time.Time `json:"created_at"`
}

type ScreenStatus struct {
	ScreenID     string    `json:"screen_id"`
	DisplayState string    `json:"display_state"`
	ReportedAt   time.Time `json:"reported_at"`
}

type ScreenSchedule struct {
	ScreenID        string     `json:"screen_id"`
	ScheduleEnabled bool       `json:"schedule_enabled"`
	PowerOnTime     string     `json:"power_on_time"`
	PowerOffTime    string     `json:"power_off_time"`
	OverrideOnUntil *time.Time `json:"override_on_until,omitempty"`
}

type MediaAsset struct {
	ID          string    `json:"id"`
	TenantID    string    `json:"tenant_id"`
	Title       string    `json:"title"`
	Type        string    `json:"type"` // image | video | pdf | web
	StoragePath string    `json:"storage_path,omitempty"`
	OriginalURL string    `json:"original_url,omitempty"`
	MimeType    string    `json:"mime_type,omitempty"`
	SizeBytes   int64     `json:"size_bytes,omitempty"`
	Enabled     bool      `json:"enabled"`
	CreatedAt   time.Time `json:"created_at"`
}

type Playlist struct {
	ID                     string    `json:"id"`
	TenantID               string    `json:"tenant_id"`
	ScreenID               string    `json:"screen_id"`
	Name                   string    `json:"name"`
	IsActive               bool      `json:"is_active"`
	DefaultDurationSeconds int       `json:"default_duration_seconds"`
	CreatedAt              time.Time `json:"created_at"`
	UpdatedAt              time.Time `json:"updated_at"`
}

// ScreenUserEntry is a lightweight view used when listing users assigned to a screen.
type ScreenUserEntry struct {
	ID        string    `json:"id"`
	Username  string    `json:"username"`
	CreatedAt time.Time `json:"created_at"`
}

type PlaylistItem struct {
	ID              string     `json:"id"`
	PlaylistID      string     `json:"playlist_id"`
	MediaAssetID    string     `json:"media_asset_id,omitempty"`
	OrderIndex      int        `json:"order_index"`
	Type            string     `json:"type"` // image | video | pdf | web
	Src             string     `json:"src"`
	Title           string     `json:"title,omitempty"`
	DurationSeconds int        `json:"duration_seconds"`
	ValidFrom       *time.Time `json:"valid_from,omitempty"`
	ValidUntil      *time.Time `json:"valid_until,omitempty"`
	Enabled         bool       `json:"enabled"`
	CreatedAt       time.Time  `json:"created_at"`
}

// ------------------------------------------------------------------
// Stores
// ------------------------------------------------------------------

type TenantStore struct{ pool *pgxpool.Pool }
type ScreenStore struct{ pool *pgxpool.Pool }
type MediaStore struct{ pool *pgxpool.Pool }
type PlaylistStore struct{ pool *pgxpool.Pool }
type ScreenScheduleStore struct{ pool *pgxpool.Pool }

func NewTenantStore(pool *pgxpool.Pool) *TenantStore   { return &TenantStore{pool} }
func NewScreenStore(pool *pgxpool.Pool) *ScreenStore   { return &ScreenStore{pool} }
func NewMediaStore(pool *pgxpool.Pool) *MediaStore     { return &MediaStore{pool} }
func NewPlaylistStore(pool *pgxpool.Pool) *PlaylistStore { return &PlaylistStore{pool} }
func NewScreenScheduleStore(pool *pgxpool.Pool) *ScreenScheduleStore {
	return &ScreenScheduleStore{pool}
}

// ------------------------------------------------------------------
// TenantStore
// ------------------------------------------------------------------

func (s *TenantStore) Get(ctx context.Context, slug string) (*Tenant, error) {
	row := s.pool.QueryRow(ctx,
		`select id, slug, name, created_at from tenants where slug=$1`, slug)
	return scanTenant(row)
}

func (s *TenantStore) List(ctx context.Context) ([]*Tenant, error) {
	rows, err := s.pool.Query(ctx, `select id, slug, name, created_at from tenants order by name`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []*Tenant
	for rows.Next() {
		t, err := scanTenant(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, t)
	}
	return out, rows.Err()
}

func scanTenant(row interface {
	Scan(dest ...any) error
}) (*Tenant, error) {
	var t Tenant
	err := row.Scan(&t.ID, &t.Slug, &t.Name, &t.CreatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan tenant: %w", err)
	}
	return &t, nil
}

// ------------------------------------------------------------------
// ScreenStore
// ------------------------------------------------------------------

func (s *ScreenStore) List(ctx context.Context, tenantID string) ([]*Screen, error) {
	rows, err := s.pool.Query(ctx,
		`select id, tenant_id, slug, name, orientation, created_at
		   from screens where tenant_id=$1 order by name`, tenantID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []*Screen
	for rows.Next() {
		sc, err := scanScreen(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, sc)
	}
	return out, rows.Err()
}

func (s *ScreenStore) ListAll(ctx context.Context) ([]*Screen, error) {
	rows, err := s.pool.Query(ctx,
		`select id, tenant_id, slug, name, orientation, created_at from screens order by name`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []*Screen
	for rows.Next() {
		sc, err := scanScreen(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, sc)
	}
	return out, rows.Err()
}

func (s *ScreenStore) GetBySlug(ctx context.Context, slug string) (*Screen, error) {
	row := s.pool.QueryRow(ctx,
		`select id, tenant_id, slug, name, orientation, created_at from screens where slug=$1`, slug)
	return scanScreen(row)
}

func (s *ScreenStore) GetByID(ctx context.Context, id string) (*Screen, error) {
	row := s.pool.QueryRow(ctx,
		`select id, tenant_id, slug, name, orientation, created_at from screens where id=$1`, id)
	return scanScreen(row)
}

func (s *ScreenStore) Create(ctx context.Context, tenantID, slug, name, orientation string) (*Screen, error) {
	row := s.pool.QueryRow(ctx,
		`insert into screens(tenant_id, slug, name, orientation)
		 values($1,$2,$3,$4)
		 returning id, tenant_id, slug, name, orientation, created_at`,
		tenantID, slug, name, orientation)
	return scanScreen(row)
}

// Upsert creates or updates a screen by slug (idempotent).
// Used by agents on startup to self-register.
func (s *ScreenStore) Upsert(ctx context.Context, tenantID, slug, name, orientation string) (*Screen, error) {
	row := s.pool.QueryRow(ctx,
		`insert into screens(tenant_id, slug, name, orientation)
		 values($1,$2,$3,$4)
		 on conflict (slug) do update
		    set name = excluded.name,
		        orientation = excluded.orientation
		 returning id, tenant_id, slug, name, orientation, created_at`,
		tenantID, slug, name, orientation)
	return scanScreen(row)
}

func (s *ScreenStore) Update(ctx context.Context, id, name, orientation string) (*Screen, error) {
	row := s.pool.QueryRow(ctx,
		`update screens set name=$2, orientation=$3 where id=$1
		 returning id, tenant_id, slug, name, orientation, created_at`,
		id, name, orientation)
	return scanScreen(row)
}

func (s *ScreenStore) Delete(ctx context.Context, id string) error {
	_, err := s.pool.Exec(ctx, `delete from screens where id=$1`, id)
	return err
}

// GetAccessibleScreens returns all screens that userID has explicit access to
// via user_screen_permissions.
func (s *ScreenStore) GetAccessibleScreens(ctx context.Context, userID string) ([]*Screen, error) {
	rows, err := s.pool.Query(ctx,
		`select sc.id, sc.tenant_id, sc.slug, sc.name, sc.orientation, sc.created_at
		   from screens sc
		   join user_screen_permissions usp on usp.screen_id = sc.id
		  where usp.user_id = $1
		  order by sc.name`, userID)
	if err != nil {
		return nil, fmt.Errorf("screens: get accessible: %w", err)
	}
	defer rows.Close()
	var out []*Screen
	for rows.Next() {
		sc, err := scanScreen(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, sc)
	}
	return out, rows.Err()
}

// HasUserScreenAccess returns true when userID has an explicit permission entry
// for screenID in user_screen_permissions.
func (s *ScreenStore) HasUserScreenAccess(ctx context.Context, userID, screenID string) (bool, error) {
	var ok bool
	err := s.pool.QueryRow(ctx,
		`select exists(
		     select 1 from user_screen_permissions
		      where user_id = $1 and screen_id = $2
		 )`, userID, screenID).Scan(&ok)
	return ok, err
}

// AddUserToScreen creates a permission entry granting userID access to screenID.
// Silently succeeds if the entry already exists (ON CONFLICT DO NOTHING).
func (s *ScreenStore) AddUserToScreen(ctx context.Context, userID, screenID string) error {
	_, err := s.pool.Exec(ctx,
		`insert into user_screen_permissions(user_id, screen_id)
		 values($1, $2)
		 on conflict (user_id, screen_id) do nothing`,
		userID, screenID)
	if err != nil {
		return fmt.Errorf("screens: add user to screen: %w", err)
	}
	return nil
}

// RemoveUserFromScreen deletes the permission entry for userID / screenID.
func (s *ScreenStore) RemoveUserFromScreen(ctx context.Context, userID, screenID string) error {
	_, err := s.pool.Exec(ctx,
		`delete from user_screen_permissions where user_id = $1 and screen_id = $2`,
		userID, screenID)
	if err != nil {
		return fmt.Errorf("screens: remove user from screen: %w", err)
	}
	return nil
}

// GetScreenUsers returns all users that have explicit access to screenID.
func (s *ScreenStore) GetScreenUsers(ctx context.Context, screenID string) ([]*ScreenUserEntry, error) {
	rows, err := s.pool.Query(ctx,
		`select u.id, u.username, u.created_at
		   from users u
		   join user_screen_permissions usp on usp.user_id = u.id
		  where usp.screen_id = $1
		  order by u.username`, screenID)
	if err != nil {
		return nil, fmt.Errorf("screens: get screen users: %w", err)
	}
	defer rows.Close()
	var out []*ScreenUserEntry
	for rows.Next() {
		var e ScreenUserEntry
		if err := rows.Scan(&e.ID, &e.Username, &e.CreatedAt); err != nil {
			return nil, fmt.Errorf("scan screen user entry: %w", err)
		}
		out = append(out, &e)
	}
	return out, rows.Err()
}

func scanScreen(row interface {
	Scan(dest ...any) error
}) (*Screen, error) {
	var sc Screen
	err := row.Scan(&sc.ID, &sc.TenantID, &sc.Slug, &sc.Name, &sc.Orientation, &sc.CreatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan screen: %w", err)
	}
	return &sc, nil
}

// UpsertDisplayState speichert den zuletzt gemeldeten Display-Zustand eines Screens.
func (s *ScreenStore) UpsertDisplayState(ctx context.Context, screenID, displayState string) error {
	_, err := s.pool.Exec(ctx,
		`insert into screen_status (screen_id, display_state, reported_at)
		 values ($1, $2, now())
		 on conflict (screen_id) do update
		   set display_state = excluded.display_state,
		       reported_at   = excluded.reported_at`,
		screenID, displayState)
	return err
}

// GetDisplayState gibt den zuletzt gemeldeten Display-Zustand zurück.
// Gibt "unknown" zurück wenn kein Eintrag vorhanden ist.
func (s *ScreenStore) GetDisplayState(ctx context.Context, screenID string) (string, error) {
	var state string
	err := s.pool.QueryRow(ctx,
		`select coalesce(display_state,'unknown')
		   from screen_status where screen_id = $1`, screenID).Scan(&state)
	if errors.Is(err, pgx.ErrNoRows) {
		return "unknown", nil
	}
	if err != nil {
		return "unknown", err
	}
	return state, nil
}

// ------------------------------------------------------------------
// MediaStore
// ------------------------------------------------------------------

func (s *MediaStore) List(ctx context.Context, tenantID string) ([]*MediaAsset, error) {
	rows, err := s.pool.Query(ctx,
		`select id, tenant_id, title, type, coalesce(storage_path,''),
		        coalesce(original_url,''), coalesce(mime_type,''), coalesce(size_bytes,0),
		        enabled, created_at
		   from media_assets where tenant_id=$1 order by created_at desc`, tenantID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []*MediaAsset
	for rows.Next() {
		m, err := scanMedia(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, m)
	}
	return out, rows.Err()
}

func (s *MediaStore) Get(ctx context.Context, id string) (*MediaAsset, error) {
	row := s.pool.QueryRow(ctx,
		`select id, tenant_id, title, type, coalesce(storage_path,''),
		        coalesce(original_url,''), coalesce(mime_type,''), coalesce(size_bytes,0),
		        enabled, created_at
		   from media_assets where id=$1`, id)
	return scanMedia(row)
}

func (s *MediaStore) Create(ctx context.Context, tenantID, title, assetType, storagePath, originalURL, mimeType string, sizeBytes int64) (*MediaAsset, error) {
	row := s.pool.QueryRow(ctx,
		`insert into media_assets(tenant_id, title, type, storage_path, original_url, mime_type, size_bytes)
		 values($1,$2,$3,nullif($4,''),nullif($5,''),nullif($6,''),nullif($7,0))
		 returning id, tenant_id, title, type, coalesce(storage_path,''),
		           coalesce(original_url,''), coalesce(mime_type,''), coalesce(size_bytes,0),
		           enabled, created_at`,
		tenantID, title, assetType, storagePath, originalURL, mimeType, sizeBytes)
	return scanMedia(row)
}

func (s *MediaStore) Delete(ctx context.Context, id string) error {
	_, err := s.pool.Exec(ctx, `delete from media_assets where id=$1`, id)
	return err
}

func scanMedia(row interface {
	Scan(dest ...any) error
}) (*MediaAsset, error) {
	var m MediaAsset
	err := row.Scan(&m.ID, &m.TenantID, &m.Title, &m.Type,
		&m.StoragePath, &m.OriginalURL, &m.MimeType, &m.SizeBytes,
		&m.Enabled, &m.CreatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan media: %w", err)
	}
	return &m, nil
}

// ------------------------------------------------------------------
// PlaylistStore
// ------------------------------------------------------------------

func (s *PlaylistStore) GetOrCreateForScreen(ctx context.Context, tenantID, screenID, screenName string) (*Playlist, error) {
	row := s.pool.QueryRow(ctx,
		`insert into playlists(tenant_id, screen_id, name)
		 values($1,$2,$3)
		 on conflict(screen_id) do update set updated_at=now()
		 returning id, tenant_id, screen_id, name, is_active, default_duration_seconds, created_at, updated_at`,
		tenantID, screenID, screenName+" Playlist")
	return scanPlaylist(row)
}

func (s *PlaylistStore) Get(ctx context.Context, id string) (*Playlist, error) {
	row := s.pool.QueryRow(ctx,
		`select id, tenant_id, screen_id, name, is_active, default_duration_seconds, created_at, updated_at
		   from playlists where id=$1`, id)
	return scanPlaylist(row)
}

func (s *PlaylistStore) GetByScreen(ctx context.Context, screenID string) (*Playlist, error) {
	row := s.pool.QueryRow(ctx,
		`select id, tenant_id, screen_id, name, is_active, default_duration_seconds, created_at, updated_at
		   from playlists where screen_id=$1`, screenID)
	return scanPlaylist(row)
}

// GetByItemID returns the playlist that contains the given playlist item.
// Used for tenant-isolation checks (K4).
func (s *PlaylistStore) GetByItemID(ctx context.Context, itemID string) (*Playlist, error) {
	row := s.pool.QueryRow(ctx,
		`select pl.id, pl.tenant_id, pl.screen_id, pl.name, pl.is_active,
		        pl.default_duration_seconds, pl.created_at, pl.updated_at
		   from playlists pl
		   join playlist_items pi on pi.playlist_id = pl.id
		  where pi.id = $1`, itemID)
	return scanPlaylist(row)
}

func (s *PlaylistStore) UpdateDefaultDuration(ctx context.Context, id string, seconds int) error {
	_, err := s.pool.Exec(ctx,
		`update playlists set default_duration_seconds=$2, updated_at=now() where id=$1`, id, seconds)
	return err
}

func scanPlaylist(row interface {
	Scan(dest ...any) error
}) (*Playlist, error) {
	var p Playlist
	err := row.Scan(&p.ID, &p.TenantID, &p.ScreenID, &p.Name,
		&p.IsActive, &p.DefaultDurationSeconds, &p.CreatedAt, &p.UpdatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan playlist: %w", err)
	}
	return &p, nil
}

// ------------------------------------------------------------------
// PlaylistItemStore (part of PlaylistStore for simplicity)
// ------------------------------------------------------------------

func (s *PlaylistStore) ListItems(ctx context.Context, playlistID string) ([]*PlaylistItem, error) {
	rows, err := s.pool.Query(ctx,
		`select id, playlist_id, coalesce(media_asset_id,''), order_index, type, src,
		        coalesce(title,''), duration_seconds, valid_from, valid_until, enabled, created_at
		   from playlist_items where playlist_id=$1 order by order_index`, playlistID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []*PlaylistItem
	for rows.Next() {
		item, err := scanPlaylistItem(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, item)
	}
	return out, rows.Err()
}

func (s *PlaylistStore) ListActiveItems(ctx context.Context, playlistID string) ([]*PlaylistItem, error) {
	rows, err := s.pool.Query(ctx,
		`select id, playlist_id, coalesce(media_asset_id,''), order_index, type, src,
		        coalesce(title,''), duration_seconds, valid_from, valid_until, enabled, created_at
		   from playlist_items
		  where playlist_id=$1
		    and enabled=true
		    and (valid_from is null or valid_from <= now())
		    and (valid_until is null or valid_until > now())
		  order by order_index`, playlistID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []*PlaylistItem
	for rows.Next() {
		item, err := scanPlaylistItem(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, item)
	}
	return out, rows.Err()
}

func (s *PlaylistStore) AddItem(ctx context.Context, playlistID, mediaAssetID, itemType, src, title string, durationSeconds int, validFrom, validUntil *time.Time) (*PlaylistItem, error) {
	var mediaID *string
	if mediaAssetID != "" {
		mediaID = &mediaAssetID
	}

	// W1: Atomare Subquery statt 2 separater Queries — verhindert Race Condition bei order_index.
	row := s.pool.QueryRow(ctx,
		`insert into playlist_items(playlist_id, media_asset_id, order_index, type, src, title, duration_seconds, valid_from, valid_until)
		 values($1,$2,
		        (select coalesce(max(order_index)+1, 0) from playlist_items where playlist_id=$1),
		        $3,$4,$5,$6,$7,$8)
		 returning id, playlist_id, coalesce(media_asset_id,''), order_index, type, src,
		           coalesce(title,''), duration_seconds, valid_from, valid_until, enabled, created_at`,
		playlistID, mediaID, itemType, src, title, durationSeconds, validFrom, validUntil)
	return scanPlaylistItem(row)
}

func (s *PlaylistStore) UpdateItem(ctx context.Context, id, title string, durationSeconds int, enabled bool, validFrom, validUntil *time.Time) error {
	_, err := s.pool.Exec(ctx,
		`update playlist_items
		    set title=$2, duration_seconds=$3, enabled=$4, valid_from=$5, valid_until=$6
		  where id=$1`,
		id, title, durationSeconds, enabled, validFrom, validUntil)
	return err
}

func (s *PlaylistStore) DeleteItem(ctx context.Context, id string) error {
	_, err := s.pool.Exec(ctx, `delete from playlist_items where id=$1`, id)
	return err
}

// ScreenSlugByPlaylistID returns the slug of the screen that owns playlistID.
func (s *PlaylistStore) ScreenSlugByPlaylistID(ctx context.Context, playlistID string) (string, error) {
	var slug string
	err := s.pool.QueryRow(ctx,
		`select sc.slug
		   from playlists pl
		   join screens sc on sc.id = pl.screen_id
		  where pl.id = $1`, playlistID).Scan(&slug)
	return slug, err
}

// ScreenSlugByItemID returns the slug of the screen that owns itemID.
func (s *PlaylistStore) ScreenSlugByItemID(ctx context.Context, itemID string) (string, error) {
	var slug string
	err := s.pool.QueryRow(ctx,
		`select sc.slug
		   from playlist_items pi
		   join playlists pl on pl.id = pi.playlist_id
		   join screens sc on sc.id = pl.screen_id
		  where pi.id = $1`, itemID).Scan(&slug)
	return slug, err
}

// Reorder sets order_index for each item ID in the given slice order.
// Returns ErrReorderMismatch if the number of provided IDs does not match
// the number of items in the playlist, or if any ID does not belong to it.
func (s *PlaylistStore) Reorder(ctx context.Context, playlistID string, itemIDs []string) error {
	seen := make(map[string]struct{}, len(itemIDs))
	for _, id := range itemIDs {
		if _, dup := seen[id]; dup {
			return fmt.Errorf("%w: duplicate id %s", ErrReorderMismatch, id)
		}
		seen[id] = struct{}{}
	}

	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx) //nolint:errcheck

	var count int
	if err := tx.QueryRow(ctx,
		`select count(*) from playlist_items where playlist_id=$1`, playlistID,
	).Scan(&count); err != nil {
		return err
	}
	if count != len(itemIDs) {
		return fmt.Errorf("%w: got %d ids, playlist has %d items",
			ErrReorderMismatch, len(itemIDs), count)
	}

	for i, id := range itemIDs {
		tag, err := tx.Exec(ctx,
			`update playlist_items set order_index=$1 where id=$2 and playlist_id=$3`,
			i, id, playlistID,
		)
		if err != nil {
			return err
		}
		if tag.RowsAffected() != 1 {
			return fmt.Errorf("%w: id %s not found in playlist %s",
				ErrReorderMismatch, id, playlistID)
		}
	}
	return tx.Commit(ctx)
}

func scanPlaylistItem(row interface {
	Scan(dest ...any) error
}) (*PlaylistItem, error) {
	var it PlaylistItem
	err := row.Scan(&it.ID, &it.PlaylistID, &it.MediaAssetID, &it.OrderIndex,
		&it.Type, &it.Src, &it.Title, &it.DurationSeconds,
		&it.ValidFrom, &it.ValidUntil, &it.Enabled, &it.CreatedAt)
	if err != nil {
		return nil, fmt.Errorf("scan playlist_item: %w", err)
	}
	return &it, nil
}

// ------------------------------------------------------------------
// ScreenScheduleStore
// ------------------------------------------------------------------

// Get lädt den Zeitplan eines Screens. Gibt einen leeren ScreenSchedule zurück wenn keiner vorhanden.
func (s *ScreenScheduleStore) Get(ctx context.Context, screenID string) (*ScreenSchedule, error) {
	var sc ScreenSchedule
	err := s.pool.QueryRow(ctx,
		`select screen_id, schedule_enabled, power_on_time, power_off_time, override_on_until
		   from screen_schedules where screen_id = $1`, screenID).
		Scan(&sc.ScreenID, &sc.ScheduleEnabled, &sc.PowerOnTime, &sc.PowerOffTime, &sc.OverrideOnUntil)
	if errors.Is(err, pgx.ErrNoRows) {
		return &ScreenSchedule{ScreenID: screenID}, nil
	}
	if err != nil {
		return nil, err
	}
	return &sc, nil
}

// Upsert speichert oder aktualisiert den Zeitplan eines Screens.
// Hinweis: override_on_until wird hier bewusst nicht angefasst – das ist
// ausschließlich Aufgabe von SetOverrideOnUntil (saubere Trennung, kein Datenverlust).
func (s *ScreenScheduleStore) Upsert(ctx context.Context, sc *ScreenSchedule) error {
	_, err := s.pool.Exec(ctx,
		`insert into screen_schedules (screen_id, schedule_enabled, power_on_time, power_off_time)
		 values ($1, $2, $3, $4)
		 on conflict (screen_id) do update
		   set schedule_enabled = excluded.schedule_enabled,
		       power_on_time    = excluded.power_on_time,
		       power_off_time   = excluded.power_off_time`,
		sc.ScreenID, sc.ScheduleEnabled, sc.PowerOnTime, sc.PowerOffTime)
	return err
}

// ListEnabled gibt alle Screens mit aktivem Zeitplan zurück.
func (s *ScreenScheduleStore) ListEnabled(ctx context.Context) ([]*ScreenSchedule, error) {
	rows, err := s.pool.Query(ctx,
		`select screen_id, schedule_enabled, power_on_time, power_off_time, override_on_until
		   from screen_schedules
		  where schedule_enabled = true
		    and (power_on_time != '' or power_off_time != '')`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []*ScreenSchedule
	for rows.Next() {
		var sc ScreenSchedule
		if err := rows.Scan(&sc.ScreenID, &sc.ScheduleEnabled, &sc.PowerOnTime, &sc.PowerOffTime, &sc.OverrideOnUntil); err != nil {
			return nil, err
		}
		out = append(out, &sc)
	}
	return out, rows.Err()
}

// SetOverrideOnUntil setzt oder löscht den per-Screen-Override (null = löschen).
func (s *ScreenScheduleStore) SetOverrideOnUntil(ctx context.Context, screenID string, until *time.Time) error {
	_, err := s.pool.Exec(ctx,
		`insert into screen_schedules (screen_id, override_on_until)
		 values ($1, $2)
		 on conflict (screen_id) do update
		   set override_on_until = excluded.override_on_until`,
		screenID, until)
	return err
}

// ------------------------------------------------------------------
// GlobalOverrideStore
// ------------------------------------------------------------------

// GlobalOverride beschreibt einen aktiven globalen Display-Override.
type GlobalOverride struct {
	Type  string    `json:"type"`   // "on" oder "off"
	Until time.Time `json:"until"`
	SetAt time.Time `json:"set_at"`
}

// GlobalOverrideStore verwaltet den globalen Display-Override (max. 1 Zeile).
type GlobalOverrideStore struct{ pool *pgxpool.Pool }

func NewGlobalOverrideStore(pool *pgxpool.Pool) *GlobalOverrideStore {
	return &GlobalOverrideStore{pool: pool}
}

// Get lädt den aktuellen globalen Override. Gibt nil zurück wenn keiner gesetzt ist.
func (s *GlobalOverrideStore) Get(ctx context.Context) (*GlobalOverride, error) {
	var o GlobalOverride
	err := s.pool.QueryRow(ctx,
		`select type, until, set_at from global_override where id = 1`).
		Scan(&o.Type, &o.Until, &o.SetAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &o, nil
}

// Upsert setzt oder überschreibt den globalen Override.
func (s *GlobalOverrideStore) Upsert(ctx context.Context, overrideType string, until time.Time) error {
	_, err := s.pool.Exec(ctx,
		`insert into global_override (id, type, until, set_at)
		 values (1, $1, $2, now())
		 on conflict (id) do update
		   set type   = excluded.type,
		       until  = excluded.until,
		       set_at = excluded.set_at`,
		overrideType, until)
	return err
}

// Delete entfernt den globalen Override.
func (s *GlobalOverrideStore) Delete(ctx context.Context) error {
	_, err := s.pool.Exec(ctx, `delete from global_override where id = 1`)
	return err
}
