using System.Runtime.CompilerServices;
using Dapper;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;

namespace ErsatzTV.Infrastructure.Data.Repositories;

public class SearchRepository(IDbContextFactory<TvContext> dbContextFactory) : ISearchRepository
{
    public async Task<Option<MediaItem>> GetItemToIndex(int id, CancellationToken cancellationToken)
    {
        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        var baseItem = await dbContext.MediaItems
            .AsNoTracking()
            .TagWithCallSite()
            .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);

        if (baseItem is null)
        {
            return Option<MediaItem>.None;
        }

        switch (baseItem)
        {
            case Movie:
                return await dbContext.Movies
                    .AsNoTracking()
                    .TagWithCallSite()
                    .IncludeForSearch()
                    .AsSplitQuery()
                    .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
            case Episode:
                return await dbContext.Episodes
                    .AsNoTracking()
                    .TagWithCallSite()
                    .IncludeForSearch()
                    .AsSplitQuery()
                    .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
            case Season:
                return await dbContext.Seasons
                    .AsNoTracking()
                    .TagWithCallSite()
                    .IncludeForSearch()
                    .AsSplitQuery()
                    .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
            case Show:
                return await dbContext.Shows
                    .AsNoTracking()
                    .TagWithCallSite()
                    .IncludeForSearch()
                    .AsSplitQuery()
                    .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
            case MusicVideo:
                return await dbContext.MusicVideos
                    .AsNoTracking()
                    .TagWithCallSite()
                    .IncludeForSearch()
                    .AsSplitQuery()
                    .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
            case Artist:
                return await dbContext.Artists
                    .AsNoTracking()
                    .TagWithCallSite()
                    .IncludeForSearch()
                    .AsSplitQuery()
                    .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
            case OtherVideo:
                return await dbContext.OtherVideos
                    .AsNoTracking()
                    .TagWithCallSite()
                    .IncludeForSearch()
                    .AsSplitQuery()
                    .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
            case Song:
                return await dbContext.Songs
                    .AsNoTracking()
                    .TagWithCallSite()
                    .IncludeForSearch()
                    .AsSplitQuery()
                    .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
            case Image:
                return await dbContext.Images
                    .AsNoTracking()
                    .TagWithCallSite()
                    .IncludeForSearch()
                    .AsSplitQuery()
                    .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
            case RemoteStream:
                return await dbContext.RemoteStreams
                    .AsNoTracking()
                    .TagWithCallSite()
                    .IncludeForSearch()
                    .AsSplitQuery()
                    .SingleOrDefaultAsync(mi => mi.Id == id, cancellationToken);
        }

        return Option<MediaItem>.None;
    }

    public async Task<List<string>> GetLanguagesForShow(Show show)
    {
        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
        return await dbContext.Connection.QueryAsync<string>(
            @"SELECT DISTINCT Language
                    FROM MediaStream
                    INNER JOIN MediaVersion MV ON MediaStream.MediaVersionId = MV.Id
                    INNER JOIN Episode E ON MV.EpisodeId = E.Id
                    INNER JOIN Season S ON E.SeasonId = S.Id
                    WHERE MediaStreamKind = 2 AND S.ShowId = @ShowId",
            new { ShowId = show.Id }).Map(result => result.ToList());
    }

    public async Task<List<string>> GetSubLanguagesForShow(Show show)
    {
        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
        return await dbContext.Connection.QueryAsync<string>(
            @"SELECT DISTINCT Language
                    FROM MediaStream
                    INNER JOIN MediaVersion MV ON MediaStream.MediaVersionId = MV.Id
                    INNER JOIN Episode E ON MV.EpisodeId = E.Id
                    INNER JOIN Season S ON E.SeasonId = S.Id
                    WHERE MediaStreamKind = 3 AND S.ShowId = @ShowId",
            new { ShowId = show.Id }).Map(result => result.ToList());
    }

    public async Task<List<string>> GetLanguagesForSeason(Season season)
    {
        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
        return await dbContext.Connection.QueryAsync<string>(
            @"SELECT DISTINCT Language
                    FROM MediaStream
                    INNER JOIN MediaVersion MV ON MediaStream.MediaVersionId = MV.Id
                    INNER JOIN Episode E ON MV.EpisodeId = E.Id
                    WHERE MediaStreamKind = 2 AND E.SeasonId = @SeasonId",
            new { SeasonId = season.Id }).Map(result => result.ToList());
    }

    public async Task<List<string>> GetSubLanguagesForSeason(Season season)
    {
        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
        return await dbContext.Connection.QueryAsync<string>(
            @"SELECT DISTINCT Language
                    FROM MediaStream
                    INNER JOIN MediaVersion MV ON MediaStream.MediaVersionId = MV.Id
                    INNER JOIN Episode E ON MV.EpisodeId = E.Id
                    WHERE MediaStreamKind = 3 AND E.SeasonId = @SeasonId",
            new { SeasonId = season.Id }).Map(result => result.ToList());
    }

    public async Task<List<string>> GetLanguagesForArtist(Artist artist)
    {
        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
        return await dbContext.Connection.QueryAsync<string>(
            @"SELECT DISTINCT Language
                    FROM MediaStream
                    INNER JOIN MediaVersion V ON MediaStream.MediaVersionId = V.Id
                    INNER JOIN MusicVideo MV ON V.MusicVideoId = MV.Id
                    INNER JOIN Artist A on MV.ArtistId = A.Id
                    WHERE MediaStreamKind = 2 AND A.Id = @ArtistId",
            new { ArtistId = artist.Id }).Map(result => result.ToList());
    }

    public async Task<List<string>> GetSubLanguagesForArtist(Artist artist)
    {
        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
        return await dbContext.Connection.QueryAsync<string>(
            @"SELECT DISTINCT Language
                    FROM MediaStream
                    INNER JOIN MediaVersion V ON MediaStream.MediaVersionId = V.Id
                    INNER JOIN MusicVideo MV ON V.MusicVideoId = MV.Id
                    INNER JOIN Artist A on MV.ArtistId = A.Id
                    WHERE MediaStreamKind = 3 AND A.Id = @ArtistId",
            new { ArtistId = artist.Id }).Map(result => result.ToList());
    }

    public async IAsyncEnumerable<MediaItem> GetAllMediaItems(
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await foreach (var item in GetAllMovies(cancellationToken))
        {
            yield return item;
        }

        await foreach (var item in GetAllShows(cancellationToken))
        {
            yield return item;
        }

        await foreach (var item in GetAllSeasons(cancellationToken))
        {
            yield return item;
        }

        await foreach (var item in GetAllEpisodes(cancellationToken))
        {
            yield return item;
        }

        await foreach (var item in GetAllMusicVideos(cancellationToken))
        {
            yield return item;
        }

        await foreach (var item in GetAllArtists(cancellationToken))
        {
            yield return item;
        }

        await foreach (var item in GetAllOtherVideos(cancellationToken))
        {
            yield return item;
        }

        await foreach (var item in GetAllSongs(cancellationToken))
        {
            yield return item;
        }

        await foreach (var item in GetAllImages(cancellationToken))
        {
            yield return item;
        }

        await foreach (var item in GetAllRemoteStreams(cancellationToken))
        {
            yield return item;
        }
    }

    private async IAsyncEnumerable<Movie> GetAllMovies([EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        ConfiguredCancelableAsyncEnumerable<Movie> movies = dbContext.Movies
            .AsNoTracking()
            .TagWithCallSite()
            .IncludeForSearch()
            .AsSplitQuery()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken);

        await foreach (var movie in movies)
        {
            yield return movie;
        }
    }

    private async IAsyncEnumerable<Show> GetAllShows([EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        ConfiguredCancelableAsyncEnumerable<Show> shows = dbContext.Shows
            .AsNoTracking()
            .TagWithCallSite()
            .IncludeForSearch()
            .AsSplitQuery()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken);

        await foreach (var movie in shows)
        {
            yield return movie;
        }
    }

    private async IAsyncEnumerable<Season> GetAllSeasons([EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        ConfiguredCancelableAsyncEnumerable<Season> seasons = dbContext.Seasons
            .AsNoTracking()
            .TagWithCallSite()
            .IncludeForSearch()
            .AsSplitQuery()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken);

        await foreach (var movie in seasons)
        {
            yield return movie;
        }
    }

    private async IAsyncEnumerable<Episode> GetAllEpisodes([EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        ConfiguredCancelableAsyncEnumerable<Episode> episodes = dbContext.Episodes
            .AsNoTracking()
            .TagWithCallSite()
            .IncludeForSearch()
            .AsSplitQuery()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken);

        await foreach (var movie in episodes)
        {
            yield return movie;
        }
    }

    private async IAsyncEnumerable<MusicVideo> GetAllMusicVideos(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        ConfiguredCancelableAsyncEnumerable<MusicVideo> musicVideos = dbContext.MusicVideos
            .AsNoTracking()
            .TagWithCallSite()
            .IncludeForSearch()
            .AsSplitQuery()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken);

        await foreach (var movie in musicVideos)
        {
            yield return movie;
        }
    }

    private async IAsyncEnumerable<Artist> GetAllArtists([EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        ConfiguredCancelableAsyncEnumerable<Artist> artists = dbContext.Artists
            .AsNoTracking()
            .TagWithCallSite()
            .IncludeForSearch()
            .AsSplitQuery()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken);

        await foreach (var movie in artists)
        {
            yield return movie;
        }
    }

    private async IAsyncEnumerable<OtherVideo> GetAllOtherVideos(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        ConfiguredCancelableAsyncEnumerable<OtherVideo> otherVideos = dbContext.OtherVideos
            .AsNoTracking()
            .TagWithCallSite()
            .IncludeForSearch()
            .AsSplitQuery()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken);

        await foreach (var movie in otherVideos)
        {
            yield return movie;
        }
    }

    private async IAsyncEnumerable<Song> GetAllSongs([EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        ConfiguredCancelableAsyncEnumerable<Song> songs = dbContext.Songs
            .AsNoTracking()
            .TagWithCallSite()
            .IncludeForSearch()
            .AsSplitQuery()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken);

        await foreach (var movie in songs)
        {
            yield return movie;
        }
    }

    private async IAsyncEnumerable<Image> GetAllImages([EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        ConfiguredCancelableAsyncEnumerable<Image> images = dbContext.Images
            .AsNoTracking()
            .TagWithCallSite()
            .IncludeForSearch()
            .AsSplitQuery()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken);

        await foreach (var movie in images)
        {
            yield return movie;
        }
    }

    private async IAsyncEnumerable<RemoteStream> GetAllRemoteStreams(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        ConfiguredCancelableAsyncEnumerable<RemoteStream> remoteStreams = dbContext.RemoteStreams
            .AsNoTracking()
            .TagWithCallSite()
            .IncludeForSearch()
            .AsSplitQuery()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken);

        await foreach (var movie in remoteStreams)
        {
            yield return movie;
        }
    }
}
