package postgres

import (
	"context"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/tdvorak/seen/backend/internal/services/catalog"
)

const listDiscoverRowsSQL = `
WITH matched AS (
    SELECT
        ds.kind,
        ds.title AS section_title,
        ds.subtitle AS section_subtitle,
        ds.display_order,
        dsi.position AS section_position,
        m.id,
        m.provider,
        m.provider_id,
        m.title,
        m.overview,
        m.media_type,
        m.release_date::text AS release_date,
        m.rating,
        m.runtime_minutes,
        m.artwork_key,
        m.platforms,
        COALESCE(array_agg(g.name ORDER BY mg.position) FILTER (WHERE g.name IS NOT NULL), '{}'::text[]) AS genres
    FROM discover_sections ds
    JOIN discover_section_items dsi ON dsi.section_kind = ds.kind
    JOIN media_items m ON m.id = dsi.media_id
    LEFT JOIN media_genres mg ON mg.media_id = m.id
    LEFT JOIN genres g ON g.id = mg.genre_id
    WHERE
        (
            $1::text = ''
            OR LOWER(m.title) LIKE '%' || LOWER($1::text) || '%'
            OR LOWER(m.overview) LIKE '%' || LOWER($1::text) || '%'
            OR EXISTS (
                SELECT 1
                FROM media_genres qmg
                JOIN genres qg ON qg.id = qmg.genre_id
                WHERE qmg.media_id = m.id
                    AND LOWER(qg.name) LIKE '%' || LOWER($1::text) || '%'
            )
        )
        AND (
            $2::text = ''
            OR EXISTS (
                SELECT 1
                FROM media_genres gmg
                JOIN genres gg ON gg.id = gmg.genre_id
                WHERE gmg.media_id = m.id
                    AND LOWER(gg.name) = LOWER($2::text)
            )
        )
        AND (
            $3::text = ''
            OR LOWER($3::text) = 'all'
            OR m.media_type = LOWER($3::text)
        )
    GROUP BY
        ds.kind,
        ds.title,
        ds.subtitle,
        ds.display_order,
        dsi.position,
        m.id,
        m.provider,
        m.provider_id,
        m.title,
        m.overview,
        m.media_type,
        m.release_date,
        m.rating,
        m.runtime_minutes,
        m.artwork_key
        ,
        m.platforms
), ranked AS (
    SELECT
        *,
        row_number() OVER (PARTITION BY kind ORDER BY section_position ASC) AS row_num
    FROM matched
)
SELECT
    kind,
    section_title,
    section_subtitle,
    display_order,
    section_position,
    id,
    provider,
    provider_id,
    title,
    overview,
    media_type,
    release_date,
    rating,
    runtime_minutes,
    artwork_key,
    platforms,
    genres
FROM ranked
WHERE row_num > $4::int
    AND row_num <= ($4::int + $5::int)
ORDER BY display_order ASC, section_position ASC;
`

const listSectionItemsByKindSQL = `
SELECT
    m.id,
    m.provider,
    m.provider_id,
    m.title,
    m.overview,
    m.media_type,
    m.release_date::text AS release_date,
    m.rating,
    m.runtime_minutes,
    m.artwork_key,
    m.platforms,
    COALESCE(array_agg(g.name ORDER BY mg.position) FILTER (WHERE g.name IS NOT NULL), '{}'::text[]) AS genres
FROM discover_section_items dsi
JOIN media_items m ON m.id = dsi.media_id
LEFT JOIN media_genres mg ON mg.media_id = m.id
LEFT JOIN genres g ON g.id = mg.genre_id
WHERE dsi.section_kind = $1::text
GROUP BY
    dsi.position,
    m.id,
    m.provider,
    m.provider_id,
    m.title,
    m.overview,
    m.media_type,
    m.release_date,
    m.rating,
    m.runtime_minutes,
    m.artwork_key,
    m.platforms
ORDER BY dsi.position ASC
LIMIT $2::int;
`

const searchMediaItemsSQL = `
SELECT
    m.id,
    m.provider,
    m.provider_id,
    m.title,
    m.overview,
    m.media_type,
    m.release_date::text AS release_date,
    m.rating,
    m.runtime_minutes,
    m.artwork_key,
    m.platforms,
    COALESCE(array_agg(g.name ORDER BY mg.position) FILTER (WHERE g.name IS NOT NULL), '{}'::text[]) AS genres
FROM media_items m
LEFT JOIN media_genres mg ON mg.media_id = m.id
LEFT JOIN genres g ON g.id = mg.genre_id
WHERE
    (
        $1::text = ''
        OR LOWER(m.title) LIKE '%' || LOWER($1::text) || '%'
        OR LOWER(m.overview) LIKE '%' || LOWER($1::text) || '%'
        OR EXISTS (
            SELECT 1
            FROM media_genres qmg
            JOIN genres qg ON qg.id = qmg.genre_id
            WHERE qmg.media_id = m.id
                AND LOWER(qg.name) LIKE '%' || LOWER($1::text) || '%'
        )
    )
    AND (
        $2::text = ''
        OR EXISTS (
            SELECT 1
            FROM media_genres gmg
            JOIN genres gg ON gg.id = gmg.genre_id
            WHERE gmg.media_id = m.id
                AND LOWER(gg.name) = LOWER($2::text)
        )
    )
    AND (
        $3::text = ''
        OR LOWER($3::text) = 'all'
        OR m.media_type = LOWER($3::text)
    )
GROUP BY
    m.id,
    m.provider,
    m.provider_id,
    m.title,
    m.overview,
    m.media_type,
    m.release_date,
    m.rating,
    m.runtime_minutes,
    m.artwork_key,
    m.platforms
ORDER BY m.rating DESC, m.release_date DESC, m.title ASC
LIMIT 50;
`

const listUserWatchLaterSQL = `
SELECT
    m.id,
    m.provider,
    m.provider_id,
    m.title,
    m.overview,
    m.media_type,
    m.release_date::text AS release_date,
    m.rating,
    m.runtime_minutes,
    m.artwork_key,
    m.platforms,
    COALESCE(array_agg(g.name ORDER BY mg.position) FILTER (WHERE g.name IS NOT NULL), '{}'::text[]) AS genres,
    uwl.created_at
FROM user_watch_later uwl
JOIN media_items m ON m.id = uwl.media_id
LEFT JOIN media_genres mg ON mg.media_id = m.id
LEFT JOIN genres g ON g.id = mg.genre_id
WHERE uwl.user_id = $1::uuid
GROUP BY
    uwl.created_at,
    m.id,
    m.provider,
    m.provider_id,
    m.title,
    m.overview,
    m.media_type,
    m.release_date,
    m.rating,
    m.runtime_minutes,
    m.artwork_key,
    m.platforms
ORDER BY uwl.created_at DESC;
`

const addUserWatchLaterSQL = `
INSERT INTO user_watch_later (user_id, media_id)
VALUES ($1::uuid, $2::bigint)
ON CONFLICT (user_id, media_id) DO NOTHING;
`

const removeUserWatchLaterSQL = `
DELETE FROM user_watch_later
WHERE user_id = $1::uuid
    AND media_id = $2::bigint;
`

const mediaExistsSQL = `
SELECT EXISTS(SELECT 1 FROM media_items WHERE id = $1::bigint);
`

const listContinueWatchingSQL = `
SELECT
    m.id,
    m.provider,
    m.provider_id,
    m.title,
    m.overview,
    m.media_type,
    m.release_date::text AS release_date,
    m.rating,
    m.runtime_minutes,
    m.artwork_key,
    m.platforms,
    COALESCE(array_agg(g.name ORDER BY mg.position) FILTER (WHERE g.name IS NOT NULL), '{}'::text[]) AS genres,
    up.season_number,
    up.episode_number,
    up.progress_percent,
    up.last_watched_at
FROM user_progress up
JOIN media_items m ON m.id = up.media_id
LEFT JOIN media_genres mg ON mg.media_id = m.id
LEFT JOIN genres g ON g.id = mg.genre_id
WHERE up.user_id = $1::uuid
    AND up.progress_percent > 0
    AND up.progress_percent < 100
GROUP BY
    up.last_watched_at,
    up.season_number,
    up.episode_number,
    up.progress_percent,
    m.id,
    m.provider,
    m.provider_id,
    m.title,
    m.overview,
    m.media_type,
    m.release_date,
    m.rating,
    m.runtime_minutes,
    m.artwork_key,
    m.platforms
ORDER BY up.last_watched_at DESC
LIMIT $2::int;
`

const upsertUserProgressSQL = `
INSERT INTO user_progress (
    user_id,
    media_id,
    season_number,
    episode_number,
    progress_percent,
    last_watched_at,
    created_at,
    updated_at
) VALUES ($1::uuid, $2::bigint, $3::int, $4::int, $5::int, NOW(), NOW(), NOW())
ON CONFLICT (user_id, media_id, season_number, episode_number)
DO UPDATE SET
    progress_percent = EXCLUDED.progress_percent,
    last_watched_at = NOW(),
    updated_at = NOW();
`

type CatalogRepository struct {
	pool *pgxpool.Pool
}

func NewCatalogRepository(pool *pgxpool.Pool) *CatalogRepository {
	return &CatalogRepository{pool: pool}
}

type scannedMediaItem struct {
	id             int64
	provider       string
	providerID     int64
	title          string
	overview       string
	mediaType      string
	releaseDate    string
	rating         float64
	runtimeMinutes int32
	artworkKey     string
	platforms      []string
	genres         []string
}

func (item scannedMediaItem) toCatalog() catalog.MediaItem {
	return catalog.MediaItem{
		ID:             int(item.id),
		Provider:       catalog.MediaProvider(item.provider),
		ProviderID:     int(item.providerID),
		Title:          item.title,
		Overview:       item.overview,
		Type:           catalog.MediaType(item.mediaType),
		ReleaseDate:    item.releaseDate,
		Genres:         cloneStrings(item.genres),
		Platforms:      cloneStrings(item.platforms),
		Rating:         item.rating,
		RuntimeMinutes: int(item.runtimeMinutes),
		ArtworkKey:     item.artworkKey,
	}
}

func (r *CatalogRepository) Discover(
	ctx context.Context,
	params catalog.DiscoverParams,
) ([]catalog.DiscoverSection, error) {
	offset := (params.Page - 1) * params.PageSize

	rows, err := r.pool.Query(
		ctx,
		listDiscoverRowsSQL,
		strings.TrimSpace(params.Query),
		strings.TrimSpace(params.Genre),
		strings.TrimSpace(params.MediaType),
		offset,
		params.PageSize,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	sections := make([]catalog.DiscoverSection, 0)
	sectionIndex := make(map[string]int)

	for rows.Next() {
		var kind string
		var sectionTitle string
		var sectionSubtitle string
		var displayOrder int16
		var sectionPosition int16
		var item scannedMediaItem

		if err := rows.Scan(
			&kind,
			&sectionTitle,
			&sectionSubtitle,
			&displayOrder,
			&sectionPosition,
			&item.id,
			&item.provider,
			&item.providerID,
			&item.title,
			&item.overview,
			&item.mediaType,
			&item.releaseDate,
			&item.rating,
			&item.runtimeMinutes,
			&item.artworkKey,
			&item.platforms,
			&item.genres,
		); err != nil {
			return nil, err
		}
		_ = displayOrder
		_ = sectionPosition

		idx, exists := sectionIndex[kind]
		if !exists {
			idx = len(sections)
			sectionIndex[kind] = idx
			sections = append(sections, catalog.DiscoverSection{
				Kind:     kind,
				Title:    sectionTitle,
				Subtitle: sectionSubtitle,
				Items:    []catalog.MediaItem{},
			})
		}

		sections[idx].Items = append(sections[idx].Items, item.toCatalog())
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return sections, nil
}

func (r *CatalogRepository) SectionItems(
	ctx context.Context,
	kind string,
	limit int,
) ([]catalog.MediaItem, error) {
	rows, err := r.pool.Query(ctx, listSectionItemsByKindSQL, strings.TrimSpace(kind), limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]catalog.MediaItem, 0, limit)
	for rows.Next() {
		var item scannedMediaItem
		if err := rows.Scan(
			&item.id,
			&item.provider,
			&item.providerID,
			&item.title,
			&item.overview,
			&item.mediaType,
			&item.releaseDate,
			&item.rating,
			&item.runtimeMinutes,
			&item.artworkKey,
			&item.platforms,
			&item.genres,
		); err != nil {
			return nil, err
		}

		items = append(items, item.toCatalog())
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return items, nil
}

func (r *CatalogRepository) SearchMedia(
	ctx context.Context,
	params catalog.SearchParams,
) ([]catalog.MediaItem, error) {
	query := strings.TrimSpace(params.Query)
	if query == "" {
		return []catalog.MediaItem{}, nil
	}

	rows, err := r.pool.Query(
		ctx,
		searchMediaItemsSQL,
		query,
		strings.TrimSpace(params.Genre),
		strings.TrimSpace(params.MediaType),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]catalog.MediaItem, 0, 32)
	for rows.Next() {
		var item scannedMediaItem
		if err := rows.Scan(
			&item.id,
			&item.provider,
			&item.providerID,
			&item.title,
			&item.overview,
			&item.mediaType,
			&item.releaseDate,
			&item.rating,
			&item.runtimeMinutes,
			&item.artworkKey,
			&item.platforms,
			&item.genres,
		); err != nil {
			return nil, err
		}

		items = append(items, item.toCatalog())
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return items, nil
}

func (r *CatalogRepository) ListWatchLater(
	ctx context.Context,
	userID uuid.UUID,
) ([]catalog.MediaItem, error) {
	rows, err := r.pool.Query(ctx, listUserWatchLaterSQL, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]catalog.MediaItem, 0, 8)
	for rows.Next() {
		var item scannedMediaItem
		var createdAt time.Time
		if err := rows.Scan(
			&item.id,
			&item.provider,
			&item.providerID,
			&item.title,
			&item.overview,
			&item.mediaType,
			&item.releaseDate,
			&item.rating,
			&item.runtimeMinutes,
			&item.artworkKey,
			&item.platforms,
			&item.genres,
			&createdAt,
		); err != nil {
			return nil, err
		}
		_ = createdAt

		items = append(items, item.toCatalog())
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return items, nil
}

func (r *CatalogRepository) AddWatchLater(
	ctx context.Context,
	userID uuid.UUID,
	mediaID int,
) error {
	if err := r.ensureMediaExists(ctx, mediaID); err != nil {
		return err
	}

	_, err := r.pool.Exec(ctx, addUserWatchLaterSQL, userID, mediaID)
	return err
}

func (r *CatalogRepository) RemoveWatchLater(
	ctx context.Context,
	userID uuid.UUID,
	mediaID int,
) error {
	_, err := r.pool.Exec(ctx, removeUserWatchLaterSQL, userID, mediaID)
	return err
}

func (r *CatalogRepository) ListContinueWatching(
	ctx context.Context,
	userID uuid.UUID,
	limit int,
) ([]catalog.ContinueWatchingItem, error) {
	rows, err := r.pool.Query(ctx, listContinueWatchingSQL, userID, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]catalog.ContinueWatchingItem, 0, limit)
	for rows.Next() {
		var item scannedMediaItem
		var seasonNumber int32
		var episodeNumber int32
		var progressPercent int32
		var lastWatchedAt time.Time

		if err := rows.Scan(
			&item.id,
			&item.provider,
			&item.providerID,
			&item.title,
			&item.overview,
			&item.mediaType,
			&item.releaseDate,
			&item.rating,
			&item.runtimeMinutes,
			&item.artworkKey,
			&item.platforms,
			&item.genres,
			&seasonNumber,
			&episodeNumber,
			&progressPercent,
			&lastWatchedAt,
		); err != nil {
			return nil, err
		}

		items = append(items, catalog.ContinueWatchingItem{
			Item: item.toCatalog(),
			Progress: catalog.EpisodeProgress{
				ItemID:          int(item.id),
				SeasonNumber:    int(seasonNumber),
				EpisodeNumber:   int(episodeNumber),
				ProgressPercent: int(progressPercent),
				LastWatchedAt:   lastWatchedAt.UTC().Format(time.RFC3339),
			},
		})
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return items, nil
}

func (r *CatalogRepository) UpsertProgress(
	ctx context.Context,
	userID uuid.UUID,
	input catalog.ProgressUpdateInput,
) error {
	if err := r.ensureMediaExists(ctx, input.MediaID); err != nil {
		return err
	}

	_, err := r.pool.Exec(
		ctx,
		upsertUserProgressSQL,
		userID,
		input.MediaID,
		input.SeasonNumber,
		input.EpisodeNumber,
		input.ProgressPercent,
	)
	return err
}

func (r *CatalogRepository) ensureMediaExists(ctx context.Context, mediaID int) error {
	var exists bool
	if err := r.pool.QueryRow(ctx, mediaExistsSQL, mediaID).Scan(&exists); err != nil {
		return err
	}

	if !exists {
		return catalog.ErrMediaNotFound
	}

	return nil
}

func cloneStrings(values []string) []string {
	if len(values) == 0 {
		return []string{}
	}

	cloned := make([]string, len(values))
	copy(cloned, values)
	return cloned
}
