using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Assets;
using Tiku.Application.Catalog;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Assets;

public sealed class VideoPlaybackService(TikuDbContext dbContext) : IVideoPlaybackService
{
    public async Task<CatalogList<VideoExplanationCatalogItem>> SearchAsync(
        VideoPlaybackActor actor,
        VideoSearchQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var videos = dbContext.VideoExplanations.AsNoTracking()
            .Where(video => video.TenantId == actor.TenantId && video.IsActive);
        if (query.SubjectId.HasValue)
            videos = videos.Where(video => video.SubjectId == query.SubjectId.Value || video.SubjectId == null);

        if (!string.IsNullOrWhiteSpace(query.Keyword))
        {
            var keyword = query.Keyword.Trim();
            videos = videos.Where(video =>
                video.Title.Contains(keyword) ||
                (video.Description != null && video.Description.Contains(keyword)));
        }

        var items = await videos
            .OrderBy(video => video.SortOrder)
            .ThenBy(video => video.Title)
            .Take(Math.Clamp(query.Limit ?? 50, 1, 200))
            .Select(video => ToVideoItem(video))
            .ToArrayAsync(cancellationToken);
        return new CatalogList<VideoExplanationCatalogItem>(items);
    }

    public async Task<VideoPlaybackItem> PlayAsync(
        VideoPlaybackActor actor,
        VideoPlayCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var video = await ResolveVideoAsync(actor.TenantId, command.VideoId, cancellationToken);
        if (command.QuestionId.HasValue)
            await AssertQuestionVideoAsync(actor.TenantId, command.QuestionId.Value, command.VideoId,
                cancellationToken);

        var progress = await ResolveProgressAsync(actor, command.VideoId, command.QuestionId, cancellationToken);
        progress.PlayCount++;
        progress.LastPlayedAt = DateTimeOffset.UtcNow;
        await dbContext.SaveChangesAsync(cancellationToken);

        dbContext.ContentAssetAccessEvents.Add(new ContentAssetAccessEvent
        {
            TenantId = actor.TenantId,
            UserId = actor.UserId,
            ActorRole = AssetAccessActorRole.Student,
            AccessType = AssetAccessType.Preview,
            AssetType = ContentAssetType.Video.ToString(),
            Result = AssetAccessResult.Granted,
            Metadata = JsonSerializer.SerializeToElement(new
            {
                videoId = video.Id,
                command.QuestionId,
                source = "student_video_play"
            })
        });
        await dbContext.SaveChangesAsync(cancellationToken);

        return new VideoPlaybackItem(
            video.Id,
            command.QuestionId,
            video.Title,
            video.Description,
            video.VideoUrl,
            video.ThumbnailUrl,
            video.DurationSeconds,
            ToProgressItem(progress),
            video.Metadata);
    }

    public async Task<VideoProgressItem> ReportProgressAsync(
        VideoPlaybackActor actor,
        VideoProgressCommand command,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var video = await ResolveVideoAsync(actor.TenantId, command.VideoId, cancellationToken);
        if (command.QuestionId.HasValue)
            await AssertQuestionVideoAsync(actor.TenantId, command.QuestionId.Value, command.VideoId,
                cancellationToken);

        var progress = await ResolveProgressAsync(actor, command.VideoId, command.QuestionId, cancellationToken);
        var positionSeconds = Math.Max(command.PositionSeconds, 0);
        var durationSeconds = command.DurationSeconds ?? video.DurationSeconds;
        var watchedSeconds = Math.Max(command.WatchedSeconds ?? positionSeconds, progress.WatchedSeconds);
        var completed = command.IsCompleted == true ||
                        (durationSeconds is > 0 && positionSeconds >= Math.Max(0, durationSeconds.Value - 3));
        progress.PositionSeconds = positionSeconds;
        progress.DurationSeconds = durationSeconds;
        progress.WatchedSeconds = watchedSeconds;
        progress.IsCompleted = completed;
        progress.CompletedAt = completed ? progress.CompletedAt ?? DateTimeOffset.UtcNow : progress.CompletedAt;
        progress.LastPlayedAt = DateTimeOffset.UtcNow;
        progress.Metadata = command.Metadata.ValueKind == JsonValueKind.Object
            ? command.Metadata.Clone()
            : JsonDefaults.Object();
        await dbContext.SaveChangesAsync(cancellationToken);
        return ToProgressItem(progress);
    }

    public async Task<CatalogList<QuestionVideoCatalogItem>> GetQuestionVideosAsync(
        VideoPlaybackActor actor,
        QuestionVideoQuery query,
        CancellationToken cancellationToken = default)
    {
        await AssertActiveMemberAsync(actor, cancellationToken);
        var questionVideos = dbContext.QuestionVideos.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId);
        if (query.QuestionId.HasValue)
            questionVideos = questionVideos.Where(item => item.QuestionId == query.QuestionId.Value);

        if (query.QuestionIds is { Count: > 0 })
            questionVideos = questionVideos.Where(item =>
                item.QuestionId.HasValue && query.QuestionIds.Contains(item.QuestionId.Value));

        var videos = dbContext.VideoExplanations.AsNoTracking()
            .Where(item => item.TenantId == actor.TenantId && item.IsActive);
        var items = await questionVideos
            .OrderBy(item => item.SortOrder)
            .ThenBy(item => item.CreatedAt)
            .Take(Math.Clamp(query.Limit ?? 100, 1, 500))
            .GroupJoin(
                videos,
                questionVideo => new { questionVideo.TenantId, Id = questionVideo.VideoId },
                video => new { video.TenantId, Id = (Guid?)video.Id },
                (questionVideo, matchedVideos) => new
                {
                    QuestionVideo = questionVideo,
                    Video = matchedVideos.FirstOrDefault()
                })
            .Select(row => new QuestionVideoCatalogItem(
                row.QuestionVideo.Id,
                row.QuestionVideo.LegacyId,
                row.QuestionVideo.QuestionId,
                row.QuestionVideo.VideoId,
                row.QuestionVideo.VideoType,
                row.QuestionVideo.SortOrder,
                row.QuestionVideo.Metadata,
                row.Video == null ? null : ToVideoItem(row.Video)))
            .ToArrayAsync(cancellationToken);
        return new CatalogList<QuestionVideoCatalogItem>(items);
    }

    private async Task<VideoPlaybackProgress> ResolveProgressAsync(
        VideoPlaybackActor actor,
        Guid videoId,
        Guid? questionId,
        CancellationToken cancellationToken)
    {
        var progress = await dbContext.VideoPlaybackProgress.SingleOrDefaultAsync(
            item =>
                item.TenantId == actor.TenantId &&
                item.UserId == actor.UserId &&
                item.VideoId == videoId &&
                item.QuestionId == questionId,
            cancellationToken);
        if (progress is not null) return progress;

        progress = new VideoPlaybackProgress
        {
            TenantId = actor.TenantId,
            UserId = actor.UserId,
            VideoId = videoId,
            QuestionId = questionId,
            LastPlayedAt = DateTimeOffset.UtcNow,
            Metadata = JsonDefaults.Object()
        };
        dbContext.VideoPlaybackProgress.Add(progress);
        return progress;
    }

    private async Task<VideoExplanation> ResolveVideoAsync(
        Guid tenantId,
        Guid videoId,
        CancellationToken cancellationToken)
    {
        return await dbContext.VideoExplanations.SingleOrDefaultAsync(
                   video => video.TenantId == tenantId && video.Id == videoId && video.IsActive,
                   cancellationToken)
               ?? throw new VideoPlaybackException("Video was not found.", "video_not_found");
    }

    private async Task AssertQuestionVideoAsync(
        Guid tenantId,
        Guid questionId,
        Guid videoId,
        CancellationToken cancellationToken)
    {
        var exists = await dbContext.QuestionVideos.AnyAsync(
            item =>
                item.TenantId == tenantId &&
                item.QuestionId == questionId &&
                item.VideoId == videoId,
            cancellationToken);
        if (!exists) throw new VideoPlaybackException("Question video was not found.", "question_video_not_found");
    }

    private async Task AssertActiveMemberAsync(VideoPlaybackActor actor, CancellationToken cancellationToken)
    {
        var exists = await dbContext.TenantMemberships.AnyAsync(
            membership =>
                membership.TenantId == actor.TenantId &&
                membership.UserId == actor.UserId &&
                membership.Status == MembershipStatus.Active,
            cancellationToken);
        if (!exists)
            throw new VideoPlaybackException("Current user is not a member of the tenant.", "video_access_denied");
    }

    private static VideoExplanationCatalogItem ToVideoItem(VideoExplanation video)
    {
        return new VideoExplanationCatalogItem(
            video.Id,
            video.LegacyId,
            video.SubjectId,
            video.Title,
            video.Description,
            video.VideoUrl,
            video.ThumbnailUrl,
            video.DurationSeconds,
            video.KnowledgeTags,
            video.IsGeneral,
            video.Difficulty,
            video.SortOrder,
            video.IsActive,
            video.Metadata);
    }

    private static VideoProgressItem ToProgressItem(VideoPlaybackProgress progress)
    {
        return new VideoProgressItem(
            progress.Id,
            progress.VideoId,
            progress.QuestionId,
            progress.PositionSeconds,
            progress.DurationSeconds,
            progress.WatchedSeconds,
            progress.IsCompleted,
            progress.CompletedAt,
            progress.LastPlayedAt,
            progress.PlayCount,
            progress.Metadata);
    }
}