using System.Net.Mime;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Ikar.Server.Data;
using Ikar.Server.Data.Entities;
using Ikar.Server.Infrastructure;
using Ikar.Server.Infrastructure.Storage;
using Ikar.Shared;

namespace Ikar.Server.Controllers;

[ApiController]
[Authorize]
[Route("api/[controller]")]
public sealed class StoriesController(
    IkarDbContext dbContext,
    PresenceTracker presenceTracker,
    AttachmentStorageService attachmentStorageService) : ControllerBase
{
    private const int MaxStoryAttachmentCount = 10;
    private const long MaxStoryUploadSizeBytes = 100L * 1024L * 1024L;

    [HttpGet]
    public async Task<ActionResult<IReadOnlyList<StoryDto>>> GetStories(CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var peerAuthorIds = await LoadStoryPeerAuthorIdsAsync(userId, cancellationToken);
        var now = DateTimeOffset.UtcNow;

        var stories = await dbContext.Stories
            .AsNoTracking()
            .Include(x => x.AuthorUser)
                .ThenInclude(x => x.Sessions)
            .Include(x => x.Attachments)
            .Include(x => x.Views)
            .Where(x =>
                x.DeletedAt == null &&
                x.ExpiresAt > now)
            .WhereVisibleTo(userId, peerAuthorIds)
            .OrderByDescending(x => x.CreatedAt)
            .Take(100)
            .ToListAsync(cancellationToken);

        return Ok(stories.Select(x => x.ToDto(presenceTracker, userId)).ToList());
    }

    [HttpGet("{storyId:guid}")]
    public async Task<ActionResult<StoryDto>> GetStory(Guid storyId, CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var peerAuthorIds = await LoadStoryPeerAuthorIdsAsync(userId, cancellationToken);
        var now = DateTimeOffset.UtcNow;

        var story = await dbContext.Stories
            .Include(x => x.AuthorUser)
                .ThenInclude(x => x.Sessions)
            .Include(x => x.Attachments)
            .Include(x => x.Views)
            .Where(x =>
                x.Id == storyId &&
                x.DeletedAt == null &&
                x.ExpiresAt > now)
            .WhereVisibleTo(userId, peerAuthorIds)
            .SingleOrDefaultAsync(cancellationToken);
        if (story is null)
        {
            return NotFound();
        }

        if (story.AuthorUserId != userId && story.Views.All(x => x.ViewerUserId != userId))
        {
            story.Views.Add(new StoryView
            {
                StoryId = story.Id,
                ViewerUserId = userId,
                ViewedAt = DateTimeOffset.UtcNow
            });
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        return Ok(story.ToDto(presenceTracker, userId));
    }

    [HttpPost]
    public async Task<ActionResult<StoryDto>> CreateStory(CreateStoryRequest request, CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var text = request.Text?.Trim() ?? string.Empty;
        if (string.IsNullOrWhiteSpace(text))
        {
            return BadRequest("Story text is required for MVP stories.");
        }

        var now = DateTimeOffset.UtcNow;
        var ttlSeconds = Math.Clamp(request.TtlSeconds, 60, 7 * 24 * 60 * 60);
        var story = new Story
        {
            AuthorUserId = userId,
            Text = text.Length <= 4096 ? text : text[..4096],
            Visibility = request.Visibility,
            HasProtectedContent = request.HasProtectedContent,
            CreatedAt = now,
            ExpiresAt = now.AddSeconds(ttlSeconds)
        };

        dbContext.Stories.Add(story);
        await AddModerationLogAsync(userId, ModerationLogTargetType.Story, story.Id, "story.created", cancellationToken);
        await dbContext.SaveChangesAsync(cancellationToken);

        var loaded = await LoadStoryForAuthorAsync(story.Id, userId, cancellationToken)
            ?? throw new InvalidOperationException("The created story could not be reloaded.");
        return Ok(loaded.ToDto(presenceTracker, userId));
    }

    [HttpPost("media")]
    [RequestFormLimits(MultipartBodyLengthLimit = MaxStoryUploadSizeBytes)]
    [RequestSizeLimit(MaxStoryUploadSizeBytes)]
    public async Task<ActionResult<StoryDto>> CreateMediaStory(
        [FromForm] UploadStoryRequest request,
        CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var files = request.Files
            .Where(file => file.Length > 0)
            .ToList();
        var text = request.Text?.Trim() ?? string.Empty;

        if (files.Count == 0 && string.IsNullOrWhiteSpace(text))
        {
            return BadRequest("Story text or media is required.");
        }

        if (files.Count > MaxStoryAttachmentCount)
        {
            return BadRequest($"A story can contain at most {MaxStoryAttachmentCount} files.");
        }

        var totalBytes = files.Sum(x => x.Length);
        if (totalBytes > MaxStoryUploadSizeBytes)
        {
            return BadRequest($"Story upload is limited to {MaxStoryUploadSizeBytes} bytes.");
        }

        var now = DateTimeOffset.UtcNow;
        var ttlSeconds = Math.Clamp(request.TtlSeconds, 60, 7 * 24 * 60 * 60);
        var story = new Story
        {
            AuthorUserId = userId,
            Text = text.Length <= 4096 ? text : text[..4096],
            Visibility = request.Visibility,
            HasProtectedContent = request.HasProtectedContent,
            CreatedAt = now,
            ExpiresAt = now.AddSeconds(ttlSeconds)
        };

        dbContext.Stories.Add(story);
        var savedAttachments = new List<StoryAttachment>();
        try
        {
            for (var index = 0; index < files.Count; index++)
            {
                var attachment = await attachmentStorageService.SaveStoryAsync(story.Id, files[index], cancellationToken, index);
                savedAttachments.Add(attachment);
                story.Attachments.Add(attachment);
            }

            await AddModerationLogAsync(userId, ModerationLogTargetType.Story, story.Id, "story.media.created", cancellationToken);
            await dbContext.SaveChangesAsync(cancellationToken);
        }
        catch (Exception ex) when (ex is IOException or InvalidOperationException)
        {
            foreach (var attachment in savedAttachments)
            {
                attachmentStorageService.DeleteIfExists(attachment.StoragePath);
            }

            return BadRequest(ex.Message);
        }

        var loaded = await LoadStoryForAuthorAsync(story.Id, userId, cancellationToken)
            ?? throw new InvalidOperationException("The created story could not be reloaded.");
        return Ok(loaded.ToDto(presenceTracker, userId));
    }

    [HttpDelete("{storyId:guid}")]
    public async Task<IActionResult> DeleteStory(Guid storyId, CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var story = await dbContext.Stories
            .Include(x => x.Attachments)
            .SingleOrDefaultAsync(x => x.Id == storyId && x.AuthorUserId == userId, cancellationToken);
        if (story is null)
        {
            return NotFound();
        }

        if (story.DeletedAt is null)
        {
            foreach (var attachment in story.Attachments)
            {
                attachmentStorageService.DeleteIfExists(attachment.StoragePath);
            }

            dbContext.StoryAttachments.RemoveRange(story.Attachments);
            story.Attachments.Clear();
            story.Text = string.Empty;
            story.DeletedAt = DateTimeOffset.UtcNow;
            await AddModerationLogAsync(userId, ModerationLogTargetType.Story, story.Id, "story.deleted", cancellationToken);
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        return NoContent();
    }

    [HttpGet("{storyId:guid}/views")]
    public async Task<ActionResult<IReadOnlyList<StoryViewDto>>> GetStoryViews(Guid storyId, CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var storyExists = await dbContext.Stories
            .AsNoTracking()
            .AnyAsync(x => x.Id == storyId && x.AuthorUserId == userId, cancellationToken);
        if (!storyExists)
        {
            return NotFound();
        }

        var views = await dbContext.StoryViews
            .AsNoTracking()
            .Include(x => x.ViewerUser)
                .ThenInclude(x => x.Sessions)
            .Where(x => x.StoryId == storyId)
            .OrderByDescending(x => x.ViewedAt)
            .Take(250)
            .ToListAsync(cancellationToken);

        return Ok(views
            .Select(x => new StoryViewDto(x.StoryId, x.ViewerUser.ToDto(presenceTracker), x.ViewedAt))
            .ToList());
    }

    [HttpGet("{storyId:guid}/attachments/{attachmentId:guid}/content")]
    public async Task<IActionResult> DownloadAttachment(Guid storyId, Guid attachmentId, CancellationToken cancellationToken)
    {
        var userId = User.GetRequiredUserId();
        var peerAuthorIds = await LoadStoryPeerAuthorIdsAsync(userId, cancellationToken);
        var now = DateTimeOffset.UtcNow;
        var attachment = await dbContext.StoryAttachments
            .Include(x => x.Story)
            .Where(
                x => x.Id == attachmentId &&
                     x.StoryId == storyId &&
                     x.Story.DeletedAt == null &&
                     x.Story.ExpiresAt > now)
            .WhereStoryVisibleTo(userId, peerAuthorIds)
            .SingleOrDefaultAsync(cancellationToken);
        if (attachment is null || !System.IO.File.Exists(attachment.StoragePath))
        {
            return NotFound();
        }

        var stream = System.IO.File.OpenRead(attachment.StoragePath);
        return File(stream, string.IsNullOrWhiteSpace(attachment.ContentType) ? MediaTypeNames.Application.Octet : attachment.ContentType, attachment.OriginalFileName);
    }

    private async Task<List<Guid>> LoadStoryPeerAuthorIdsAsync(Guid userId, CancellationToken cancellationToken)
    {
        var peerIds = await dbContext.ChatMembers
            .AsNoTracking()
            .Where(x => x.UserId == userId)
            .Join(
                dbContext.ChatMembers.AsNoTracking(),
                mine => mine.ChatId,
                other => other.ChatId,
                (_, other) => other.UserId)
            .Distinct()
            .ToListAsync(cancellationToken);

        return peerIds
            .Where(id => id != userId)
            .Distinct()
            .ToList();
    }

    private async Task<Story?> LoadStoryForAuthorAsync(Guid storyId, Guid userId, CancellationToken cancellationToken) =>
        await dbContext.Stories
            .Include(x => x.AuthorUser)
                .ThenInclude(x => x.Sessions)
            .Include(x => x.Attachments)
            .Include(x => x.Views)
            .SingleOrDefaultAsync(x => x.Id == storyId && x.AuthorUserId == userId, cancellationToken);

    private async Task AddModerationLogAsync(
        Guid actorUserId,
        ModerationLogTargetType targetType,
        Guid targetId,
        string action,
        CancellationToken cancellationToken)
    {
        dbContext.ModerationLogEntries.Add(new ModerationLogEntry
        {
            ActorUserId = actorUserId,
            TargetType = targetType,
            TargetId = targetId,
            Action = action,
            CreatedAt = DateTimeOffset.UtcNow
        });

        await Task.CompletedTask;
    }
}

internal static class StoryVisibilityQueryExtensions
{
    public static IQueryable<Story> WhereVisibleTo(
        this IQueryable<Story> query,
        Guid userId,
        IReadOnlyCollection<Guid> peerAuthorIds) =>
        query.Where(story =>
            story.AuthorUserId == userId ||
            story.Visibility == StoryVisibility.Everyone ||
            ((story.Visibility == StoryVisibility.Contacts || story.Visibility == StoryVisibility.CloseFriends) &&
                peerAuthorIds.Contains(story.AuthorUserId)));

    public static IQueryable<StoryAttachment> WhereStoryVisibleTo(
        this IQueryable<StoryAttachment> query,
        Guid userId,
        IReadOnlyCollection<Guid> peerAuthorIds) =>
        query.Where(attachment =>
            attachment.Story.AuthorUserId == userId ||
            attachment.Story.Visibility == StoryVisibility.Everyone ||
            ((attachment.Story.Visibility == StoryVisibility.Contacts ||
                    attachment.Story.Visibility == StoryVisibility.CloseFriends) &&
                peerAuthorIds.Contains(attachment.Story.AuthorUserId)));
}
