// Package store contains all database repositories.
package store

import (
	"context"
	"fmt"
	"time"

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

// ------------------------------------------------------------------
// 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 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"`
}

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 }

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} }

// ------------------------------------------------------------------
// 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) 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
}

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
}

// ------------------------------------------------------------------
// 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)
}

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) {
	// Place at end of list.
	var maxIdx int
	s.pool.QueryRow(ctx,
		`select coalesce(max(order_index)+1, 0) from playlist_items where playlist_id=$1`, playlistID,
	).Scan(&maxIdx) //nolint:errcheck

	var mediaID *string
	if mediaAssetID != "" {
		mediaID = &mediaAssetID
	}

	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,$3,$4,$5,$6,$7,$8,$9)
		 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, maxIdx, 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.
func (s *PlaylistStore) Reorder(ctx context.Context, playlistID string, itemIDs []string) error {
	tx, err := s.pool.Begin(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx) //nolint:errcheck

	for i, id := range itemIDs {
		if _, err := tx.Exec(ctx,
			`update playlist_items set order_index=$1 where id=$2 and playlist_id=$3`,
			i, id, playlistID,
		); err != nil {
			return err
		}
	}
	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
}
